5 # A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
13 # Copyright (c) 2004-2006 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
27 require 'booh/gtkadds'
28 require 'booh/GtkAutoTable'
32 bindtextdomain("booh")
34 require 'rexml/document'
37 require 'booh/booh-lib'
39 require 'booh/UndoHandler'
44 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
46 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
49 #- default values for some globals
53 $ignore_videos = false
54 $button1_pressed_autotable = false
55 $generated_outofline = false
58 puts _("Usage: %s [OPTION]...") % File.basename($0)
60 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
65 parser = GetoptLong.new
66 parser.set_options(*$options.collect { |ary| ary[0..2] })
68 parser.each_option do |name, arg|
74 when '--verbose-level'
75 $verbose_level = arg.to_i
88 $config_file = File.expand_path('~/.booh-gui-rc')
89 if File.readable?($config_file)
90 $xmldoc = REXML::Document.new(File.new($config_file))
91 $xmldoc.root.elements.each { |element|
92 txt = element.get_text
94 if txt.value =~ /~~~/ || element.name == 'last-opens'
95 $config[element.name] = txt.value.split(/~~~/)
97 $config[element.name] = txt.value
99 elsif element.elements.size == 0
100 $config[element.name] = ''
102 $config[element.name] = {}
103 element.each { |chld|
105 $config[element.name][chld.name] = txt ? txt.value : nil
110 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
111 $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
112 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
113 $config['comments-format'] ||= '%t'
114 if !FileTest.directory?(File.expand_path('~/.booh'))
115 system("mkdir ~/.booh")
123 if !system("which convert >/dev/null 2>/dev/null")
124 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
125 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
128 if !system("which identify >/dev/null 2>/dev/null")
129 show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
130 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
132 missing = %w(transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
134 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
137 viewer_binary = $config['video-viewer'].split.first
138 if viewer_binary && !File.executable?(viewer_binary)
139 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
140 You should fix this in Edit/Preferences so that you can view videos.
142 Problem was: '%s' is not an executable file.
143 Hint: don't forget to specify the full path to the executable,
144 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
146 image_editor_binary = $config['image-editor'].split.first
147 if image_editor_binary && !File.executable?(image_editor_binary)
148 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
149 You should fix this in Edit/Preferences so that you can edit images externally.
151 Problem was: '%s' is not an executable file.
152 Hint: don't forget to specify the full path to the executable,
153 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
155 browser_binary = $config['browser'].split.first
156 if browser_binary && !File.executable?(browser_binary)
157 show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
158 You should fix this in Edit/Preferences so that you can open URLs.
160 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
165 if $config['last-opens'] && $config['last-opens'].size > 10
166 $config['last-opens'] = $config['last-opens'][-10, 10]
169 ios = File.open($config_file, "w")
170 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
171 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
172 $config.each_pair { |key, value|
173 elem = $xmldoc.root.add_element key
175 $config[key].each_pair { |subkey, subvalue|
176 subelem = elem.add_element subkey
177 subelem.add_text subvalue.to_s
179 elsif value.is_a? Array
180 elem.add_text value.join('~~~')
185 elem.add_text value.to_s
189 $xmldoc.write(ios, 0)
192 $tempfiles.each { |f|
197 def set_mousecursor(what, *widget)
198 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
199 if widget[0] && widget[0].window
200 widget[0].window.cursor = cursor
202 if $main_window && $main_window.window
203 $main_window.window.cursor = cursor
205 $current_cursor = what
207 def set_mousecursor_wait(*widget)
208 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
209 if Thread.current == Thread.main
210 Gtk.main_iteration while Gtk.events_pending?
213 def set_mousecursor_normal(*widget)
214 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
216 def push_mousecursor_wait(*widget)
217 if $current_cursor != Gdk::Cursor::WATCH
218 $save_cursor = $current_cursor
219 gtk_thread_protect { set_mousecursor_wait(*widget) }
222 def pop_mousecursor(*widget)
223 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
227 source = $xmldoc.root.attributes['source']
228 dest = $xmldoc.root.attributes['destination']
229 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
232 def full_src_dir_to_rel(path, source)
233 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
236 def build_full_dest_filename(filename)
237 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
240 def save_undo(name, closure, *params)
241 UndoHandler.save_undo(name, closure, [ *params ])
242 $undo_tb.sensitive = $undo_mb.sensitive = true
243 $redo_tb.sensitive = $redo_mb.sensitive = false
246 def view_element(filename, closures)
247 if entry2type(filename) == 'video'
248 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
254 w = Gtk::Window.new.set_title(filename)
256 msg 3, "filename: #{filename}"
257 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
258 #- typically this file won't exist in case of videos; try with the largest thumbnail around
259 if !File.exists?(dest_img)
260 if entry2type(filename) == 'video'
261 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
262 if not alternatives.empty?
263 dest_img = alternatives[-1]
266 push_mousecursor_wait
267 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
269 if !File.exists?(dest_img)
270 msg 2, _("Could not generate fullscreen thumbnail!")
275 evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(Gtk::Image.new(dest_img)).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
276 evt.signal_connect('button-press-event') { |this, event|
277 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
278 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
280 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
282 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
283 delete_item.signal_connect('activate') {
285 closures[:delete].call(false)
288 menu.popup(nil, nil, event.button, event.time)
291 evt.signal_connect('button-release-event') { |this, event|
293 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
294 msg 3, "gesture delete: click-drag right button to the bottom"
296 closures[:delete].call(false)
297 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
301 tooltips = Gtk::Tooltips.new
302 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
304 w.signal_connect('key-press-event') { |w,event|
305 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
307 closures[:delete].call(false)
311 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
312 b.signal_connect('clicked') { w.destroy }
315 vb.pack_start(evt, false, false)
316 vb.pack_end(bottom, false, false)
319 w.signal_connect('delete-event') { w.destroy }
320 w.window_position = Gtk::Window::POS_CENTER
324 def scroll_upper(scrolledwindow, ypos_top)
325 newval = scrolledwindow.vadjustment.value -
326 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
327 if newval < scrolledwindow.vadjustment.lower
328 newval = scrolledwindow.vadjustment.lower
330 scrolledwindow.vadjustment.value = newval
333 def scroll_lower(scrolledwindow, ypos_bottom)
334 newval = scrolledwindow.vadjustment.value +
335 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
336 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
337 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
339 scrolledwindow.vadjustment.value = newval
342 def autoscroll_if_needed(scrolledwindow, image, textview)
343 #- autoscroll if cursor or image is not visible, if possible
344 if image && image.window || textview.window
345 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
346 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
347 current_miny_visible = scrolledwindow.vadjustment.value
348 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
349 if ypos_top < current_miny_visible
350 scroll_upper(scrolledwindow, ypos_top)
351 elsif ypos_bottom > current_maxy_visible
352 scroll_lower(scrolledwindow, ypos_bottom)
357 def create_editzone(scrolledwindow, pagenum, image)
358 frame = Gtk::Frame.new
359 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
360 frame.set_shadow_type(Gtk::SHADOW_IN)
361 textview.signal_connect('key-press-event') { |w, event|
362 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
363 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
364 scrolledwindow.signal_emit('key-press-event', event)
366 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
367 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
368 if event.keyval == Gdk::Keyval::GDK_Up
369 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
370 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
372 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
375 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
376 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
378 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
385 candidate_undo_text = nil
386 textview.signal_connect('focus-in-event') { |w, event|
387 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
388 candidate_undo_text = textview.buffer.text
392 textview.signal_connect('key-release-event') { |w, event|
393 if candidate_undo_text && candidate_undo_text != textview.buffer.text
395 save_undo(_("text edit"),
397 save_text = textview.buffer.text
398 textview.buffer.text = text
400 $notebook.set_page(pagenum)
402 textview.buffer.text = save_text
404 $notebook.set_page(pagenum)
406 }, candidate_undo_text)
407 candidate_undo_text = nil
410 if event.state != 0 || ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
411 autoscroll_if_needed(scrolledwindow, image, textview)
416 return [ frame, textview ]
419 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
421 if !$modified_pixbufs[thumbnail_img]
422 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
423 elsif !$modified_pixbufs[thumbnail_img][:orig]
424 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
427 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
430 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
431 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
432 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
433 if pixbuf.height > desired_y
434 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
435 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
436 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
441 if $modified_pixbufs[thumbnail_img][:whitebalance]
442 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
445 #- fix gamma correction
446 if $modified_pixbufs[thumbnail_img][:gammacorrect]
447 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
450 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
453 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
456 #- update rotate attribute
457 xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
459 $modified_pixbufs[thumbnail_img] ||= {}
460 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
461 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
463 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
466 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
469 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
471 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
473 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
474 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
476 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
477 $notebook.set_page(0)
478 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
483 def color_swap(xmldir, attributes_prefix)
485 if xmldir.attributes["#{attributes_prefix}color-swap"]
486 xmldir.delete_attribute("#{attributes_prefix}color-swap")
488 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
492 def enhance(xmldir, attributes_prefix)
494 if xmldir.attributes["#{attributes_prefix}enhance"]
495 xmldir.delete_attribute("#{attributes_prefix}enhance")
497 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
501 def change_frame_offset(xmldir, attributes_prefix, value)
503 xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
506 def ask_new_frame_offset(xmldir, attributes_prefix)
508 value = xmldir.attributes["#{attributes_prefix}frame-offset"]
513 dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
515 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
516 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
517 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
521 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
522 from. There are approximately 25 frames per second in a video.
525 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
526 entry.signal_connect('key-press-event') { |w, event|
527 if event.keyval == Gdk::Keyval::GDK_Return
528 dialog.response(Gtk::Dialog::RESPONSE_OK)
530 elsif event.keyval == Gdk::Keyval::GDK_Escape
531 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
534 false #- propagate if needed
538 dialog.window_position = Gtk::Window::POS_MOUSE
541 dialog.run { |response|
544 if response == Gtk::Dialog::RESPONSE_OK
546 msg 3, "changing frame offset to #{newval}"
547 return { :old => value, :new => newval }
554 def change_pano_amount(xmldir, attributes_prefix, value)
557 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
559 xmldir.add_attribute("#{attributes_prefix}pano-amount", value)
563 def ask_new_pano_amount(xmldir, attributes_prefix)
565 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
570 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
572 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
573 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
574 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
578 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
579 of this panorama image compared to other regular images. For example, if the panorama
580 was taken out of four photos on one row, counting the necessary overlap, the width of
581 this panorama image should probably be roughly three times the width of regular images.
583 With this information, booh will be able to generate panorama thumbnails looking
587 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("none (not a panorama image)")))).
588 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
589 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
590 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
591 spin.signal_connect('value-changed') {
594 dialog.window_position = Gtk::Window::POS_MOUSE
597 spin.value = value.to_f
604 dialog.run { |response|
608 newval = spin.value.to_f
611 if response == Gtk::Dialog::RESPONSE_OK
613 msg 3, "changing panorama amount to #{newval}"
614 return { :old => value, :new => newval }
621 def change_whitebalance(xmlelem, attributes_prefix, value)
623 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
626 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
628 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
629 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
630 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
631 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
632 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
633 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
634 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
635 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
636 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
637 $modified_pixbufs[thumbnail_img] ||= {}
638 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
639 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
641 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
642 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
644 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
647 $modified_pixbufs[thumbnail_img] ||= {}
648 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
650 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
653 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
654 #- init $modified_pixbufs correctly
655 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
657 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
659 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
661 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
662 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
663 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
667 _("You can fix the <b>white balance</b> of the image, if your image is too blue
668 or too yellow because your camera didn't detect the light correctly. Drag the
669 slider below the image to the left for more blue, to the right for more yellow.
673 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
675 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
677 dialog.window_position = Gtk::Window::POS_MOUSE
681 timeout = Gtk.timeout_add(100) {
682 if hs.value != lastval
685 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
691 dialog.run { |response|
692 Gtk.timeout_remove(timeout)
693 if response == Gtk::Dialog::RESPONSE_OK
695 newval = hs.value.to_s
696 msg 3, "changing white balance to #{newval}"
698 return { :old => value, :new => newval }
701 $modified_pixbufs[thumbnail_img] ||= {}
702 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
703 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
711 def change_gammacorrect(xmlelem, attributes_prefix, value)
713 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
716 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
718 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
719 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
720 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
721 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
722 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
723 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
724 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
725 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
726 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
727 $modified_pixbufs[thumbnail_img] ||= {}
728 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
729 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
731 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
732 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
734 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
737 $modified_pixbufs[thumbnail_img] ||= {}
738 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
740 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
743 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
744 #- init $modified_pixbufs correctly
745 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
747 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
749 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
751 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
752 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
753 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
757 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
758 or too bright. Drag the slider below the image.
762 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
764 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
766 dialog.window_position = Gtk::Window::POS_MOUSE
770 timeout = Gtk.timeout_add(100) {
771 if hs.value != lastval
774 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
780 dialog.run { |response|
781 Gtk.timeout_remove(timeout)
782 if response == Gtk::Dialog::RESPONSE_OK
784 newval = hs.value.to_s
785 msg 3, "gamma correction to #{newval}"
787 return { :old => value, :new => newval }
790 $modified_pixbufs[thumbnail_img] ||= {}
791 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
792 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
800 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
801 system("rm -f '#{destfile}'")
802 #- type can be 'element' or 'subdir'
804 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
806 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
810 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
812 push_mousecursor_wait
813 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
816 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
822 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
823 distribute_multiple_call = Proc.new { |action, arg|
824 $selected_elements.each_key { |path|
825 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
827 if possible_actions[:can_multiple] && $selected_elements.length > 0
828 UndoHandler.begin_batch
829 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
830 UndoHandler.end_batch
832 closures[action].call(arg)
834 $selected_elements = {}
837 if optionals.include?('change_image')
838 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
839 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
840 changeimg.signal_connect('activate') { closures[:change].call }
841 menu.append(Gtk::SeparatorMenuItem.new)
843 if !possible_actions[:can_multiple] || $selected_elements.length == 0
846 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
847 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
848 view.signal_connect('activate') { closures[:view].call }
850 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
851 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
852 view.signal_connect('activate') { closures[:view].call }
853 menu.append(Gtk::SeparatorMenuItem.new)
856 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
857 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
858 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
859 exif.signal_connect('activate') { show_popup($main_window,
860 utf8(`identify -format "%[EXIF:*]" #{fullpath}`.sub(/MakerNote.*\n/, '')),
861 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
862 menu.append(Gtk::SeparatorMenuItem.new)
865 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
866 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
867 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
868 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
869 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
870 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
871 if !possible_actions[:can_multiple] || $selected_elements.length == 0
872 menu.append(Gtk::SeparatorMenuItem.new)
873 if !possible_actions[:forbid_left]
874 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
875 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
876 moveleft.signal_connect('activate') { closures[:move].call('left') }
877 if !possible_actions[:can_left]
878 moveleft.sensitive = false
881 if !possible_actions[:forbid_right]
882 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
883 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
884 moveright.signal_connect('activate') { closures[:move].call('right') }
885 if !possible_actions[:can_right]
886 moveright.sensitive = false
889 if optionals.include?('move_top')
890 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
891 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
892 movetop.signal_connect('activate') { closures[:move].call('top') }
893 if !possible_actions[:can_top]
894 movetop.sensitive = false
897 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
898 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
899 moveup.signal_connect('activate') { closures[:move].call('up') }
900 if !possible_actions[:can_up]
901 moveup.sensitive = false
903 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
904 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
905 movedown.signal_connect('activate') { closures[:move].call('down') }
906 if !possible_actions[:can_down]
907 movedown.sensitive = false
909 if optionals.include?('move_bottom')
910 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
911 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
912 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
913 if !possible_actions[:can_bottom]
914 movebottom.sensitive = false
919 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
920 menu.append(Gtk::SeparatorMenuItem.new)
921 menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
922 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
923 color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
924 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
925 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
926 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
927 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
928 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
929 frame_offset.signal_connect('activate') {
930 if possible_actions[:can_multiple] && $selected_elements.length > 0
931 if values = ask_new_frame_offset(nil, '')
932 distribute_multiple_call.call(:frame_offset, values)
935 closures[:frame_offset].call
940 menu.append( Gtk::SeparatorMenuItem.new)
941 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
942 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
943 gammacorrect.signal_connect('activate') {
944 if possible_actions[:can_multiple] && $selected_elements.length > 0
945 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
946 distribute_multiple_call.call(:gammacorrect, values)
949 closures[:gammacorrect].call
952 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
953 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
954 whitebalance.signal_connect('activate') {
955 if possible_actions[:can_multiple] && $selected_elements.length > 0
956 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
957 distribute_multiple_call.call(:whitebalance, values)
960 closures[:whitebalance].call
963 if !possible_actions[:can_multiple] || $selected_elements.length == 0
964 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
965 _("Enhance constrast"))))
967 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
969 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
970 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
971 if type == 'image' && possible_actions[:can_panorama]
972 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
973 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
974 panorama.signal_connect('activate') {
975 if possible_actions[:can_multiple] && $selected_elements.length > 0
976 if values = ask_new_pano_amount(nil, '')
977 distribute_multiple_call.call(:pano, values)
980 distribute_multiple_call.call(:pano)
984 menu.append( Gtk::SeparatorMenuItem.new)
985 if optionals.include?('delete')
986 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
987 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
988 if !possible_actions[:can_multiple] || $selected_elements.length == 0
989 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
990 paste_item.signal_connect('activate') { closures[:paste].call }
991 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
992 clear_item.signal_connect('activate') { $cuts = [] }
994 paste_item.sensitive = clear_item.sensitive = false
997 menu.append( Gtk::SeparatorMenuItem.new)
999 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1000 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1001 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1002 editexternally.signal_connect('activate') {
1003 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1008 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1009 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1010 if optionals.include?('delete')
1011 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1012 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1015 menu.popup(nil, nil, event.button, event.time)
1018 def delete_current_subalbum
1020 sel = $albums_tv.selection.selected_rows
1021 $xmldir.elements.each { |e|
1022 if e.name == 'image' || e.name == 'video'
1023 e.add_attribute('deleted', 'true')
1026 #- branch if we have a non deleted subalbum
1027 if $xmldir.child_byname_notattr('dir', 'deleted')
1028 $xmldir.delete_attribute('thumbnails-caption')
1029 $xmldir.delete_attribute('thumbnails-captionfile')
1031 $xmldir.add_attribute('deleted', 'true')
1033 while moveup.parent.name == 'dir'
1034 moveup = moveup.parent
1035 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1036 moveup.add_attribute('deleted', 'true')
1043 save_changes('forced')
1044 populate_subalbums_treeview(false)
1045 $albums_tv.selection.select_path(sel[0])
1051 $current_path = nil #- prevent save_changes from being rerun again
1052 sel = $albums_tv.selection.selected_rows
1053 restore_one = proc { |xmldir|
1054 xmldir.elements.each { |e|
1055 if e.name == 'dir' && e.attributes['deleted']
1058 e.delete_attribute('deleted')
1061 restore_one.call($xmldir)
1062 populate_subalbums_treeview(false)
1063 $albums_tv.selection.select_path(sel[0])
1066 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1069 frame1 = Gtk::Frame.new
1070 fullpath = from_utf8("#{$current_path}/#{filename}")
1072 my_gen_real_thumbnail = proc {
1073 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1077 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1078 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1079 pack_start(img = Gtk::Image.new).
1080 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1081 px, mask = pxb.render_pixmap_and_mask(0)
1082 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1083 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1085 frame1.add(img = Gtk::Image.new)
1088 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1089 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1090 my_gen_real_thumbnail.call
1092 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1095 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1097 tooltips = Gtk::Tooltips.new
1098 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1099 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1101 frame2, textview = create_editzone($autotable_sw, 1, img)
1102 textview.buffer.text = caption
1103 textview.set_justification(Gtk::Justification::CENTER)
1105 vbox = Gtk::VBox.new(false, 5)
1106 vbox.pack_start(evtbox, false, false)
1107 vbox.pack_start(frame2, false, false)
1108 autotable.append(vbox, filename)
1110 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1111 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1113 #- to be able to find widgets by name
1114 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1116 cleanup_all_thumbnails = proc {
1117 #- remove out of sync images
1118 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1119 for sizeobj in $images_size
1120 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
1126 cleanup_all_thumbnails.call
1127 my_gen_real_thumbnail.call
1130 rotate_and_cleanup = proc { |angle|
1131 cleanup_all_thumbnails.call
1132 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1135 move = proc { |direction|
1136 do_method = "move_#{direction}"
1137 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1139 done = autotable.method(do_method).call(vbox)
1140 textview.grab_focus #- because if moving, focus is stolen
1144 save_undo(_("move %s") % direction,
1146 autotable.method(undo_method).call(vbox)
1147 textview.grab_focus #- because if moving, focus is stolen
1148 autoscroll_if_needed($autotable_sw, img, textview)
1149 $notebook.set_page(1)
1151 autotable.method(do_method).call(vbox)
1152 textview.grab_focus #- because if moving, focus is stolen
1153 autoscroll_if_needed($autotable_sw, img, textview)
1154 $notebook.set_page(1)
1160 color_swap_and_cleanup = proc {
1161 perform_color_swap_and_cleanup = proc {
1162 cleanup_all_thumbnails.call
1163 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1164 my_gen_real_thumbnail.call
1167 perform_color_swap_and_cleanup.call
1169 save_undo(_("color swap"),
1171 perform_color_swap_and_cleanup.call
1173 autoscroll_if_needed($autotable_sw, img, textview)
1174 $notebook.set_page(1)
1176 perform_color_swap_and_cleanup.call
1178 autoscroll_if_needed($autotable_sw, img, textview)
1179 $notebook.set_page(1)
1184 change_frame_offset_and_cleanup_real = proc { |values|
1185 perform_change_frame_offset_and_cleanup = proc { |val|
1186 cleanup_all_thumbnails.call
1187 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
1188 my_gen_real_thumbnail.call
1190 perform_change_frame_offset_and_cleanup.call(values[:new])
1192 save_undo(_("specify frame offset"),
1194 perform_change_frame_offset_and_cleanup.call(values[:old])
1196 autoscroll_if_needed($autotable_sw, img, textview)
1197 $notebook.set_page(1)
1199 perform_change_frame_offset_and_cleanup.call(values[:new])
1201 autoscroll_if_needed($autotable_sw, img, textview)
1202 $notebook.set_page(1)
1207 change_frame_offset_and_cleanup = proc {
1208 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
1209 change_frame_offset_and_cleanup_real.call(values)
1213 change_pano_amount_and_cleanup_real = proc { |values|
1214 perform_change_pano_amount_and_cleanup = proc { |val|
1215 cleanup_all_thumbnails.call
1216 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1218 perform_change_pano_amount_and_cleanup.call(values[:new])
1220 save_undo(_("change panorama amount"),
1222 perform_change_pano_amount_and_cleanup.call(values[:old])
1224 autoscroll_if_needed($autotable_sw, img, textview)
1225 $notebook.set_page(1)
1227 perform_change_pano_amount_and_cleanup.call(values[:new])
1229 autoscroll_if_needed($autotable_sw, img, textview)
1230 $notebook.set_page(1)
1235 change_pano_amount_and_cleanup = proc {
1236 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1237 change_pano_amount_and_cleanup_real.call(values)
1241 whitebalance_and_cleanup_real = proc { |values|
1242 perform_change_whitebalance_and_cleanup = proc { |val|
1243 cleanup_all_thumbnails.call
1244 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1245 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1246 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1248 perform_change_whitebalance_and_cleanup.call(values[:new])
1250 save_undo(_("fix white balance"),
1252 perform_change_whitebalance_and_cleanup.call(values[:old])
1254 autoscroll_if_needed($autotable_sw, img, textview)
1255 $notebook.set_page(1)
1257 perform_change_whitebalance_and_cleanup.call(values[:new])
1259 autoscroll_if_needed($autotable_sw, img, textview)
1260 $notebook.set_page(1)
1265 whitebalance_and_cleanup = proc {
1266 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1267 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1268 whitebalance_and_cleanup_real.call(values)
1272 gammacorrect_and_cleanup_real = proc { |values|
1273 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1274 cleanup_all_thumbnails.call
1275 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1276 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1277 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1279 perform_change_gammacorrect_and_cleanup.call(values[:new])
1281 save_undo(_("gamma correction"),
1283 perform_change_gammacorrect_and_cleanup.call(values[:old])
1285 autoscroll_if_needed($autotable_sw, img, textview)
1286 $notebook.set_page(1)
1288 perform_change_gammacorrect_and_cleanup.call(values[:new])
1290 autoscroll_if_needed($autotable_sw, img, textview)
1291 $notebook.set_page(1)
1296 gammacorrect_and_cleanup = Proc.new {
1297 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1298 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1299 gammacorrect_and_cleanup_real.call(values)
1303 enhance_and_cleanup = proc {
1304 perform_enhance_and_cleanup = proc {
1305 cleanup_all_thumbnails.call
1306 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1307 my_gen_real_thumbnail.call
1310 cleanup_all_thumbnails.call
1311 perform_enhance_and_cleanup.call
1313 save_undo(_("enhance"),
1315 perform_enhance_and_cleanup.call
1317 autoscroll_if_needed($autotable_sw, img, textview)
1318 $notebook.set_page(1)
1320 perform_enhance_and_cleanup.call
1322 autoscroll_if_needed($autotable_sw, img, textview)
1323 $notebook.set_page(1)
1328 delete = proc { |isacut|
1329 if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed? This operation cannot be undone.")), { :okcancel => true })
1332 perform_delete = proc {
1333 after = autotable.get_next_widget(vbox)
1335 after = autotable.get_previous_widget(vbox)
1337 if $config['deleteondisk'] && !isacut
1338 msg 3, "scheduling for delete: #{fullpath}"
1339 $todelete << fullpath
1341 autotable.remove(vbox)
1343 $vbox2widgets[after][:textview].grab_focus
1344 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1348 previous_pos = autotable.get_current_number(vbox)
1352 delete_current_subalbum
1354 save_undo(_("delete"),
1356 autotable.reinsert(pos, vbox, filename)
1357 $notebook.set_page(1)
1358 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1360 msg 3, "removing deletion schedule of: #{fullpath}"
1361 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1364 $notebook.set_page(1)
1373 $cuts << { :vbox => vbox, :filename => filename }
1374 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1379 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1382 autotable.queue_draws << proc {
1383 $vbox2widgets[last[:vbox]][:textview].grab_focus
1384 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1386 save_undo(_("paste"),
1388 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1389 $notebook.set_page(1)
1392 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1394 $notebook.set_page(1)
1397 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1402 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1403 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1404 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1405 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1407 textview.signal_connect('key-press-event') { |w, event|
1410 x, y = autotable.get_current_pos(vbox)
1411 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1412 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1413 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1414 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1416 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1417 $vbox2widgets[widget_up][:textview].grab_focus
1424 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1426 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1427 $vbox2widgets[widget_down][:textview].grab_focus
1434 if event.keyval == Gdk::Keyval::GDK_Left
1437 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1444 rotate_and_cleanup.call(-90)
1447 if event.keyval == Gdk::Keyval::GDK_Right
1448 next_ = autotable.get_next_widget(vbox)
1449 if next_ && autotable.get_current_pos(next_)[0] > x
1451 $vbox2widgets[next_][:textview].grab_focus
1458 rotate_and_cleanup.call(90)
1461 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1464 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1465 view_element(filename, { :delete => delete })
1468 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1471 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1475 !propagate #- propagate if needed
1478 $ignore_next_release = false
1479 evtbox.signal_connect('button-press-event') { |w, event|
1480 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1481 if event.state & Gdk::Window::BUTTON3_MASK != 0
1482 #- gesture redo: hold right mouse button then click left mouse button
1483 $config['nogestures'] or perform_redo
1484 $ignore_next_release = true
1486 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1488 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1490 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1491 elsif $enhance.active?
1492 enhance_and_cleanup.call
1493 elsif $delete.active?
1497 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1500 $button1_pressed_autotable = true
1501 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1502 if event.state & Gdk::Window::BUTTON1_MASK != 0
1503 #- gesture undo: hold left mouse button then click right mouse button
1504 $config['nogestures'] or perform_undo
1505 $ignore_next_release = true
1507 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1508 view_element(filename, { :delete => delete })
1513 evtbox.signal_connect('button-release-event') { |w, event|
1514 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1515 if !$ignore_next_release
1516 x, y = autotable.get_current_pos(vbox)
1517 next_ = autotable.get_next_widget(vbox)
1518 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1519 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1520 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1521 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1522 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1523 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1524 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1526 $ignore_next_release = false
1527 $gesture_press = nil
1532 #- handle reordering with drag and drop
1533 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1534 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1535 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1536 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1539 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1541 #- mouse gesture first (dnd disables button-release-event)
1542 if $gesture_press && $gesture_press[:filename] == filename
1543 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1544 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1545 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1546 rotate_and_cleanup.call(angle)
1547 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1549 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1550 msg 3, "gesture delete: click-drag right button to the bottom"
1552 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1557 ctxt.targets.each { |target|
1558 if target.name == 'reorder-elements'
1559 move_dnd = proc { |from,to|
1562 autotable.move(from, to)
1563 save_undo(_("reorder"),
1566 autotable.move(to - 1, from)
1568 autotable.move(to, from + 1)
1570 $notebook.set_page(1)
1572 autotable.move(from, to)
1573 $notebook.set_page(1)
1578 if $multiple_dnd.size == 0
1579 move_dnd.call(selection_data.data.to_i,
1580 autotable.get_current_number(vbox))
1582 UndoHandler.begin_batch
1583 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1585 #- need to update current position between each call
1586 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1587 autotable.get_current_number(vbox))
1589 UndoHandler.end_batch
1600 def create_auto_table
1602 $autotable = Gtk::AutoTable.new(5)
1604 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1605 thumbnails_vb = Gtk::VBox.new(false, 5)
1607 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1608 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1609 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1610 thumbnails_vb.add($autotable)
1612 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1613 $autotable_sw.add_with_viewport(thumbnails_vb)
1615 #- follows stuff for handling multiple elements selection
1616 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1618 update_selected = proc {
1619 $autotable.current_order.each { |path|
1620 w = $name2widgets[path][:evtbox].window
1621 xm = w.position[0] + w.size[0]/2
1622 ym = w.position[1] + w.size[1]/2
1623 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1624 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1625 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1626 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1629 if $selected_elements[path] && ! $selected_elements[path][:keep]
1630 if ((xm < press_x && xm < pos_x || xm > pos_x && xm > press_x) || (ym < press_y && ym < pos_y || ym > pos_y && ym > press_y))
1631 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1632 $selected_elements.delete(path)
1637 $autotable.signal_connect('realize') { |w,e|
1638 gc = Gdk::GC.new($autotable.window)
1639 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1640 gc.function = Gdk::GC::INVERT
1641 #- autoscroll handling for DND and multiple selections
1642 Gtk.timeout_add(100) {
1643 if ! $autotable.window.nil?
1644 w, x, y, mask = $autotable.window.pointer
1645 if mask & Gdk::Window::BUTTON1_MASK != 0
1646 if y < $autotable_sw.vadjustment.value
1648 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1650 if $button1_pressed_autotable || press_x
1651 scroll_upper($autotable_sw, y)
1654 w, pos_x, pos_y = $autotable.window.pointer
1655 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1656 update_selected.call
1659 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1661 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1663 if $button1_pressed_autotable || press_x
1664 scroll_lower($autotable_sw, y)
1667 w, pos_x, pos_y = $autotable.window.pointer
1668 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1669 update_selected.call
1674 ! $autotable.window.nil?
1678 $autotable.signal_connect('button-press-event') { |w,e|
1680 if !$button1_pressed_autotable
1683 if e.state & Gdk::Window::SHIFT_MASK == 0
1684 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1685 $selected_elements = {}
1686 $statusbar.push(0, utf8(_("Nothing selected.")))
1688 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1690 set_mousecursor(Gdk::Cursor::TCROSS)
1694 $autotable.signal_connect('button-release-event') { |w,e|
1696 if $button1_pressed_autotable
1697 #- unselect all only now
1698 $multiple_dnd = $selected_elements.keys
1699 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1700 $selected_elements = {}
1701 $button1_pressed_autotable = false
1704 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1705 if $selected_elements.length > 0
1706 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1709 press_x = press_y = pos_x = pos_y = nil
1710 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1714 $autotable.signal_connect('motion-notify-event') { |w,e|
1717 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1721 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1722 update_selected.call
1728 def create_subalbums_page
1730 subalbums_hb = Gtk::HBox.new
1731 $subalbums_vb = Gtk::VBox.new(false, 5)
1732 subalbums_hb.pack_start($subalbums_vb, false, false)
1733 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1734 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1735 $subalbums_sw.add_with_viewport(subalbums_hb)
1738 def save_current_file
1744 ios = File.open($filename, "w")
1745 $xmldoc.write(ios, 0)
1747 rescue Iconv::IllegalSequence
1748 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1749 if ! ios.nil? && ! ios.closed?
1752 $xmldoc.xml_decl.encoding = 'UTF-8'
1753 ios = File.open($filename, "w")
1754 $xmldoc.write(ios, 0)
1765 def save_current_file_user
1766 save_tempfilename = $filename
1767 $filename = $orig_filename
1768 if ! save_current_file
1769 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1770 $filename = save_tempfilename
1774 $generated_outofline = false
1775 $filename = save_tempfilename
1777 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1778 $todelete.each { |f|
1779 system("rm -f #{f}")
1783 def mark_document_as_dirty
1784 $xmldoc.elements.each('//dir') { |elem|
1785 elem.delete_attribute('already-generated')
1789 #- ret: true => ok false => cancel
1790 def ask_save_modifications(msg1, msg2, *options)
1792 options = options.size > 0 ? options[0] : {}
1794 if options[:disallow_cancel]
1795 dialog = Gtk::Dialog.new(msg1,
1797 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1798 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1799 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1801 dialog = Gtk::Dialog.new(msg1,
1803 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1804 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1805 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1806 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1808 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1809 dialog.vbox.add(Gtk::Label.new(msg2))
1810 dialog.window_position = Gtk::Window::POS_CENTER
1813 dialog.run { |response|
1815 if response == Gtk::Dialog::RESPONSE_YES
1816 if ! save_current_file_user
1817 return ask_save_modifications(msg1, msg2, options)
1820 #- if we have generated an album but won't save modifications, we must remove
1821 #- already-generated markers in original file
1822 if $generated_outofline
1824 $xmldoc = REXML::Document.new File.new($orig_filename)
1825 mark_document_as_dirty
1826 ios = File.open($orig_filename, "w")
1827 $xmldoc.write(ios, 0)
1830 puts "exception: #{$!}"
1834 if response == Gtk::Dialog::RESPONSE_CANCEL
1837 $todelete = [] #- unconditionally clear the list of images/videos to delete
1843 def try_quit(*options)
1844 if ask_save_modifications(utf8(_("Save before quitting?")),
1845 utf8(_("Do you want to save your changes before quitting?")),
1851 def show_popup(parent, msg, *options)
1852 dialog = Gtk::Dialog.new
1853 if options[0] && options[0][:title]
1854 dialog.title = options[0][:title]
1856 dialog.title = utf8(_("Booh message"))
1858 lbl = Gtk::Label.new
1859 if options[0] && options[0][:nomarkup]
1864 if options[0] && options[0][:centered]
1865 lbl.set_justify(Gtk::Justification::CENTER)
1867 if options[0] && options[0][:selectable]
1868 lbl.selectable = true
1870 if options[0] && options[0][:topwidget]
1871 dialog.vbox.add(options[0][:topwidget])
1873 if options[0] && options[0][:scrolled]
1874 sw = Gtk::ScrolledWindow.new(nil, nil)
1875 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1876 sw.add_with_viewport(lbl)
1878 dialog.set_default_size(500, 600)
1880 dialog.vbox.add(lbl)
1881 dialog.set_default_size(200, 120)
1883 if options[0] && options[0][:okcancel]
1884 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1886 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1888 if options[0] && options[0][:pos_centered]
1889 dialog.window_position = Gtk::Window::POS_CENTER
1891 dialog.window_position = Gtk::Window::POS_MOUSE
1894 if options[0] && options[0][:linkurl]
1895 linkbut = Gtk::Button.new('')
1896 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1897 linkbut.signal_connect('clicked') {
1898 open_url(options[0][:linkurl] + '/index.html')
1899 dialog.response(Gtk::Dialog::RESPONSE_OK)
1900 set_mousecursor_normal
1902 linkbut.relief = Gtk::RELIEF_NONE
1903 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1904 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1905 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1910 if !options[0] || !options[0][:not_transient]
1911 dialog.transient_for = parent
1912 dialog.run { |response|
1914 if options[0] && options[0][:okcancel]
1915 return response == Gtk::Dialog::RESPONSE_OK
1919 dialog.signal_connect('response') { dialog.destroy }
1923 def backend_wait_message(parent, msg, infopipe_path, mode)
1925 w.set_transient_for(parent)
1928 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1929 vb.pack_start(Gtk::Label.new(msg), false, false)
1931 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1932 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1933 if mode != 'one dir scan'
1934 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1936 if mode == 'web-album'
1937 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1938 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1940 vb.pack_start(Gtk::HSeparator.new, false, false)
1942 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1943 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1944 vb.pack_end(bottom, false, false)
1946 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1947 refresh_thread = Thread.new {
1948 directories_counter = 0
1949 while line = infopipe.gets
1950 if line =~ /^directories: (\d+), sizes: (\d+)/
1951 directories = $1.to_f + 1
1953 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1954 elements = $3.to_f + 1
1955 if mode == 'web-album'
1959 gtk_thread_protect { pb1_1.fraction = 0 }
1960 if mode != 'one dir scan'
1961 newtext = utf8(full_src_dir_to_rel($1, $2))
1962 newtext = '/' if newtext == ''
1963 gtk_thread_protect { pb1_2.text = newtext }
1964 directories_counter += 1
1965 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1967 elsif line =~ /^processing element$/
1968 element_counter += 1
1969 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1970 elsif line =~ /^processing size$/
1971 element_counter += 1
1972 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1973 elsif line =~ /^finished processing sizes$/
1974 gtk_thread_protect { pb1_1.fraction = 1 }
1975 elsif line =~ /^creating index.html$/
1976 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1977 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1978 directories_counter = 0
1979 elsif line =~ /^index.html: (.+)\|(.+)/
1980 newtext = utf8(full_src_dir_to_rel($1, $2))
1981 newtext = '/' if newtext == ''
1982 gtk_thread_protect { pb2.text = newtext }
1983 directories_counter += 1
1984 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1985 elsif line =~ /^die: (.*)$/
1992 w.signal_connect('delete-event') { w.destroy }
1993 w.signal_connect('destroy') {
1994 Thread.kill(refresh_thread)
1995 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1998 system("rm -f #{infopipe_path}")
2001 w.window_position = Gtk::Window::POS_CENTER
2007 def call_backend(cmd, waitmsg, mode, params)
2008 pipe = Tempfile.new("boohpipe")
2010 system("mkfifo #{pipe.path}")
2011 cmd += " --info-pipe #{pipe.path}"
2012 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2017 id, exitstatus = Process.waitpid2(pid)
2018 gtk_thread_protect { w8.destroy }
2020 if params[:successmsg]
2021 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2023 if params[:closure_after]
2024 gtk_thread_protect(¶ms[:closure_after])
2026 elsif exitstatus == 15
2027 #- say nothing, user aborted
2029 gtk_thread_protect { show_popup($main_window,
2030 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2036 button.signal_connect('clicked') {
2037 Process.kill('SIGTERM', pid)
2041 def save_changes(*forced)
2042 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2046 $xmldir.delete_attribute('already-generated')
2048 propagate_children = proc { |xmldir|
2049 if xmldir.attributes['subdirs-caption']
2050 xmldir.delete_attribute('already-generated')
2052 xmldir.elements.each('dir') { |element|
2053 propagate_children.call(element)
2057 if $xmldir.child_byname_notattr('dir', 'deleted')
2058 new_title = $subalbums_title.buffer.text
2059 if new_title != $xmldir.attributes['subdirs-caption']
2060 parent = $xmldir.parent
2061 if parent.name == 'dir'
2062 parent.delete_attribute('already-generated')
2064 propagate_children.call($xmldir)
2066 $xmldir.add_attribute('subdirs-caption', new_title)
2067 $xmldir.elements.each('dir') { |element|
2068 if !element.attributes['deleted']
2069 path = element.attributes['path']
2070 newtext = $subalbums_edits[path][:editzone].buffer.text
2071 if element.attributes['subdirs-caption']
2072 if element.attributes['subdirs-caption'] != newtext
2073 propagate_children.call(element)
2075 element.add_attribute('subdirs-caption', newtext)
2076 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2078 if element.attributes['thumbnails-caption'] != newtext
2079 element.delete_attribute('already-generated')
2081 element.add_attribute('thumbnails-caption', newtext)
2082 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2088 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2089 if $xmldir.attributes['thumbnails-caption']
2090 path = $xmldir.attributes['path']
2091 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2093 elsif $xmldir.attributes['thumbnails-caption']
2094 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2097 if $xmldir.attributes['thumbnails-caption']
2098 if edit = $subalbums_edits[$xmldir.attributes['path']]
2099 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2103 #- remove and reinsert elements to reflect new ordering
2106 $xmldir.elements.each { |element|
2107 if element.name == 'image' || element.name == 'video'
2108 saves[element.attributes['filename']] = element.remove
2112 $autotable.current_order.each { |path|
2113 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2114 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2117 saves.each_key { |path|
2118 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2119 chld.add_attribute('deleted', 'true')
2123 def sort_by_exif_date
2127 $xmldir.elements.each { |element|
2128 if element.name == 'image' || element.name == 'video'
2129 current_order << element.attributes['filename']
2133 #- look for EXIF dates
2135 w.set_transient_for($main_window)
2137 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2138 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2139 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2140 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2141 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2142 vb.pack_end(bottom, false, false)
2144 w.signal_connect('delete-event') { w.destroy }
2145 w.window_position = Gtk::Window::POS_CENTER
2149 b.signal_connect('clicked') { aborted = true }
2152 current_order.each { |f|
2154 if entry2type(f) == 'image'
2156 pb.fraction = i.to_f / current_order.size
2157 Gtk.main_iteration while Gtk.events_pending?
2158 date_time = `identify -format "%[EXIF:DateTimeOriginal]" '#{from_utf8($current_path + "/" + f)}'`.chomp
2159 if $? == 0 && date_time != ''
2160 dates[f] = date_time
2173 $xmldir.elements.each { |element|
2174 if element.name == 'image' || element.name == 'video'
2175 saves[element.attributes['filename']] = element.remove
2179 #- find a good fallback for all entries without a date (still next to the item they were next to)
2180 neworder = dates.keys.sort { |a,b| dates[a] <=> dates[b] }
2181 for i in 0 .. current_order.size - 1
2182 if ! neworder.include?(current_order[i])
2184 while j > 0 && ! neworder.include?(current_order[j])
2187 neworder[(neworder.index(current_order[j]) || -1 ) + 1, 0] = current_order[i]
2191 $xmldir.add_element(saves[f].name, saves[f].attributes)
2194 #- let the auto-table reflect new ordering
2198 def remove_all_captions
2201 $autotable.current_order.each { |path|
2202 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2203 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2205 save_undo(_("remove all captions"),
2207 texts.each_key { |key|
2208 $name2widgets[key][:textview].buffer.text = texts[key]
2210 $notebook.set_page(1)
2212 texts.each_key { |key|
2213 $name2widgets[key][:textview].buffer.text = ''
2215 $notebook.set_page(1)
2221 $selected_elements.each_key { |path|
2222 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2228 $selected_elements = {}
2232 $undo_tb.sensitive = $undo_mb.sensitive = false
2233 $redo_tb.sensitive = $redo_mb.sensitive = false
2239 $subalbums_vb.children.each { |chld|
2240 $subalbums_vb.remove(chld)
2242 $subalbums = Gtk::Table.new(0, 0, true)
2243 current_y_sub_albums = 0
2245 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2246 $subalbums_edits = {}
2247 subalbums_counter = 0
2248 subalbums_edits_bypos = {}
2250 add_subalbum = proc { |xmldir, counter|
2251 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2252 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2253 if xmldir == $xmldir
2254 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2255 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2256 caption = xmldir.attributes['thumbnails-caption']
2257 infotype = 'thumbnails'
2259 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2260 captionfile, caption = find_subalbum_caption_info(xmldir)
2261 infotype = find_subalbum_info_type(xmldir)
2263 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2264 hbox = Gtk::HBox.new
2265 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2267 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2270 my_gen_real_thumbnail = proc {
2271 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2274 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2275 f.add(img = Gtk::Image.new)
2276 my_gen_real_thumbnail.call
2278 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2280 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2281 $subalbums.attach(hbox,
2282 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2284 frame, textview = create_editzone($subalbums_sw, 0, img)
2285 textview.buffer.text = caption
2286 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2287 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2289 change_image = proc {
2290 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2292 Gtk::FileChooser::ACTION_OPEN,
2294 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2295 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2296 fc.transient_for = $main_window
2297 fc.preview_widget = preview = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(f = Gtk::Frame.new.set_shadow_type(Gtk::SHADOW_ETCHED_OUT))
2298 f.add(preview_img = Gtk::Image.new)
2300 fc.signal_connect('update-preview') { |w|
2302 if fc.preview_filename
2303 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2304 fc.preview_widget_active = true
2306 rescue Gdk::PixbufError
2307 fc.preview_widget_active = false
2310 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2312 old_file = captionfile
2313 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2314 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2315 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2316 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2318 new_file = fc.filename
2319 msg 3, "new captionfile is: #{fc.filename}"
2320 perform_changefile = proc {
2321 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2322 $modified_pixbufs.delete(thumbnail_file)
2323 xmldir.delete_attribute("#{infotype}-rotate")
2324 xmldir.delete_attribute("#{infotype}-color-swap")
2325 xmldir.delete_attribute("#{infotype}-enhance")
2326 xmldir.delete_attribute("#{infotype}-frame-offset")
2327 my_gen_real_thumbnail.call
2329 perform_changefile.call
2331 save_undo(_("change caption file for sub-album"),
2333 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2334 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2335 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2336 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2337 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2338 my_gen_real_thumbnail.call
2339 $notebook.set_page(0)
2341 perform_changefile.call
2342 $notebook.set_page(0)
2350 system("rm -f '#{thumbnail_file}'")
2351 my_gen_real_thumbnail.call
2354 rotate_and_cleanup = proc { |angle|
2355 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2356 system("rm -f '#{thumbnail_file}'")
2359 move = proc { |direction|
2362 save_changes('forced')
2363 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2364 if direction == 'up'
2365 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2366 subalbums_edits_bypos[oldpos - 1][:position] += 1
2368 if direction == 'down'
2369 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2370 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2372 if direction == 'top'
2373 for i in 1 .. oldpos - 1
2374 subalbums_edits_bypos[i][:position] += 1
2376 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2378 if direction == 'bottom'
2379 for i in oldpos + 1 .. subalbums_counter
2380 subalbums_edits_bypos[i][:position] -= 1
2382 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2386 $xmldir.elements.each('dir') { |element|
2387 if (!element.attributes['deleted'])
2388 elems << [ element.attributes['path'], element.remove ]
2391 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2392 each { |e| $xmldir.add_element(e[1]) }
2393 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2394 $xmldir.elements.each('descendant::dir') { |elem|
2395 elem.delete_attribute('already-generated')
2398 sel = $albums_tv.selection.selected_rows
2400 populate_subalbums_treeview(false)
2401 $albums_tv.selection.select_path(sel[0])
2404 color_swap_and_cleanup = proc {
2405 perform_color_swap_and_cleanup = proc {
2406 color_swap(xmldir, "#{infotype}-")
2407 my_gen_real_thumbnail.call
2409 perform_color_swap_and_cleanup.call
2411 save_undo(_("color swap"),
2413 perform_color_swap_and_cleanup.call
2414 $notebook.set_page(0)
2416 perform_color_swap_and_cleanup.call
2417 $notebook.set_page(0)
2422 change_frame_offset_and_cleanup = proc {
2423 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2424 perform_change_frame_offset_and_cleanup = proc { |val|
2425 change_frame_offset(xmldir, "#{infotype}-", val)
2426 my_gen_real_thumbnail.call
2428 perform_change_frame_offset_and_cleanup.call(values[:new])
2430 save_undo(_("specify frame offset"),
2432 perform_change_frame_offset_and_cleanup.call(values[:old])
2433 $notebook.set_page(0)
2435 perform_change_frame_offset_and_cleanup.call(values[:new])
2436 $notebook.set_page(0)
2442 whitebalance_and_cleanup = proc {
2443 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2444 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2445 perform_change_whitebalance_and_cleanup = proc { |val|
2446 change_whitebalance(xmldir, "#{infotype}-", val)
2447 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2448 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2449 system("rm -f '#{thumbnail_file}'")
2451 perform_change_whitebalance_and_cleanup.call(values[:new])
2453 save_undo(_("fix white balance"),
2455 perform_change_whitebalance_and_cleanup.call(values[:old])
2456 $notebook.set_page(0)
2458 perform_change_whitebalance_and_cleanup.call(values[:new])
2459 $notebook.set_page(0)
2465 gammacorrect_and_cleanup = proc {
2466 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2467 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2468 perform_change_gammacorrect_and_cleanup = proc { |val|
2469 change_gammacorrect(xmldir, "#{infotype}-", val)
2470 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2471 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2472 system("rm -f '#{thumbnail_file}'")
2474 perform_change_gammacorrect_and_cleanup.call(values[:new])
2476 save_undo(_("gamma correction"),
2478 perform_change_gammacorrect_and_cleanup.call(values[:old])
2479 $notebook.set_page(0)
2481 perform_change_gammacorrect_and_cleanup.call(values[:new])
2482 $notebook.set_page(0)
2488 enhance_and_cleanup = proc {
2489 perform_enhance_and_cleanup = proc {
2490 enhance(xmldir, "#{infotype}-")
2491 my_gen_real_thumbnail.call
2494 perform_enhance_and_cleanup.call
2496 save_undo(_("enhance"),
2498 perform_enhance_and_cleanup.call
2499 $notebook.set_page(0)
2501 perform_enhance_and_cleanup.call
2502 $notebook.set_page(0)
2507 evtbox.signal_connect('button-press-event') { |w, event|
2508 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2510 rotate_and_cleanup.call(90)
2512 rotate_and_cleanup.call(-90)
2513 elsif $enhance.active?
2514 enhance_and_cleanup.call
2517 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2518 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2519 { :forbid_left => true, :forbid_right => true,
2520 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2521 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2522 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2523 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2524 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2526 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2531 evtbox.signal_connect('button-press-event') { |w, event|
2532 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2536 evtbox.signal_connect('button-release-event') { |w, event|
2537 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2538 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2539 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2540 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2541 msg 3, "gesture rotate: #{angle}"
2542 rotate_and_cleanup.call(angle)
2545 $gesture_press = nil
2548 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2549 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2550 current_y_sub_albums += 1
2553 if $xmldir.child_byname_notattr('dir', 'deleted')
2555 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2556 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2557 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2558 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2559 #- this album image/caption
2560 if $xmldir.attributes['thumbnails-caption']
2561 add_subalbum.call($xmldir, 0)
2564 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2565 $xmldir.elements.each { |element|
2566 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2567 #- element (image or video) of this album
2568 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2569 msg 3, "dest_img: #{dest_img}"
2570 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2571 total[element.name] += 1
2573 if element.name == 'dir' && !element.attributes['deleted']
2574 #- sub-album image/caption
2575 add_subalbum.call(element, subalbums_counter += 1)
2576 total[element.name] += 1
2579 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2580 total['image'], total['video'], total['dir'] ]))
2581 $subalbums_vb.add($subalbums)
2582 $subalbums_vb.show_all
2584 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2585 $notebook.get_tab_label($autotable_sw).sensitive = false
2586 $notebook.set_page(0)
2587 $thumbnails_title.buffer.text = ''
2589 $notebook.get_tab_label($autotable_sw).sensitive = true
2590 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2593 if !$xmldir.child_byname_notattr('dir', 'deleted')
2594 $notebook.get_tab_label($subalbums_sw).sensitive = false
2595 $notebook.set_page(1)
2597 $notebook.get_tab_label($subalbums_sw).sensitive = true
2601 def pixbuf_or_nil(filename)
2603 return Gdk::Pixbuf.new(filename)
2609 def theme_choose(current)
2610 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2612 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2613 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2614 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2616 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2617 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2618 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2619 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2620 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2621 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2622 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2623 treeview.signal_connect('button-press-event') { |w, event|
2624 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2625 dialog.response(Gtk::Dialog::RESPONSE_OK)
2629 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2631 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2634 iter[0] = File.basename(dir)
2635 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2636 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2637 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2638 if File.basename(dir) == current
2639 treeview.selection.select_iter(iter)
2643 dialog.set_default_size(700, 400)
2644 dialog.vbox.show_all
2645 dialog.run { |response|
2646 iter = treeview.selection.selected
2648 if response == Gtk::Dialog::RESPONSE_OK && iter
2649 return model.get_value(iter, 0)
2655 def show_password_protections
2656 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2657 child_iter = $albums_iters[xmldir.attributes['path']]
2658 if xmldir.attributes['password-protect']
2659 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2660 already_protected = true
2661 elsif already_protected
2662 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2664 pix = pix.saturate_and_pixelate(1, true)
2670 xmldir.elements.each('dir') { |elem|
2671 if !elem.attributes['deleted']
2672 examine_dir_elem.call(child_iter, elem, already_protected)
2676 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2679 def populate_subalbums_treeview(select_first)
2683 $subalbums_vb.children.each { |chld|
2684 $subalbums_vb.remove(chld)
2687 source = $xmldoc.root.attributes['source']
2688 msg 3, "source: #{source}"
2690 xmldir = $xmldoc.elements['//dir']
2691 if !xmldir || xmldir.attributes['path'] != source
2692 msg 1, _("Corrupted booh file...")
2696 append_dir_elem = proc { |parent_iter, xmldir|
2697 child_iter = $albums_ts.append(parent_iter)
2698 child_iter[0] = File.basename(xmldir.attributes['path'])
2699 child_iter[1] = xmldir.attributes['path']
2700 $albums_iters[xmldir.attributes['path']] = child_iter
2701 msg 3, "puttin location: #{xmldir.attributes['path']}"
2702 xmldir.elements.each('dir') { |elem|
2703 if !elem.attributes['deleted']
2704 append_dir_elem.call(child_iter, elem)
2708 append_dir_elem.call(nil, xmldir)
2709 show_password_protections
2711 $albums_tv.expand_all
2713 $albums_tv.selection.select_iter($albums_ts.iter_first)
2717 def select_current_theme
2718 select_theme($xmldoc.root.attributes['theme'],
2719 $xmldoc.root.attributes['limit-sizes'],
2720 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2721 $xmldoc.root.attributes['thumbnails-per-row'])
2724 def open_file(filename)
2728 $current_path = nil #- invalidate
2729 $modified_pixbufs = {}
2732 $subalbums_vb.children.each { |chld|
2733 $subalbums_vb.remove(chld)
2736 if !File.exists?(filename)
2737 return utf8(_("File not found."))
2741 $xmldoc = REXML::Document.new File.new(filename)
2746 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2747 if entry2type(filename).nil?
2748 return utf8(_("Not a booh file!"))
2750 return utf8(_("Not a booh file!\n\nHint: you cannot import directly an image or video with File/Open.\nUse File/New to create a new album."))
2754 if !source = $xmldoc.root.attributes['source']
2755 return utf8(_("Corrupted booh file..."))
2758 if !dest = $xmldoc.root.attributes['destination']
2759 return utf8(_("Corrupted booh file..."))
2762 if !theme = $xmldoc.root.attributes['theme']
2763 return utf8(_("Corrupted booh file..."))
2766 if $xmldoc.root.attributes['version'] < '0.8.6'
2767 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2768 mark_document_as_dirty
2769 if $xmldoc.root.attributes['version'] < '0.8.4'
2770 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2771 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2772 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2773 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2774 if old_dest_dir != new_dest_dir
2775 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2777 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2778 xmldir.elements.each { |element|
2779 if %w(image video).include?(element.name) && !element.attributes['deleted']
2780 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2781 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2782 Dir[old_name + '*'].each { |file|
2783 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2784 file != new_file and sys("mv '#{file}' '#{new_file}'")
2787 if element.name == 'dir' && !element.attributes['deleted']
2788 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2789 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2790 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2794 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2798 $xmldoc.root.add_attribute('version', $VERSION)
2801 select_current_theme
2803 $filename = filename
2804 $default_size['thumbnails'] =~ /(.*)x(.*)/
2805 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2806 $albums_thumbnail_size =~ /(.*)x(.*)/
2807 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2809 populate_subalbums_treeview(true)
2811 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
2815 def open_file_user(filename)
2816 result = open_file(filename)
2818 $config['last-opens'] ||= []
2819 if $config['last-opens'][-1] != utf8(filename)
2820 $config['last-opens'] << utf8(filename)
2822 $orig_filename = $filename
2823 tmp = Tempfile.new("boohtemp")
2826 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2828 $tempfiles << $filename << "#{$filename}.backup"
2830 $orig_filename = nil
2836 if !ask_save_modifications(utf8(_("Save this album?")),
2837 utf8(_("Do you want to save the changes to this album?")),
2838 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2841 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2843 Gtk::FileChooser::ACTION_OPEN,
2845 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2846 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2847 fc.set_current_folder(File.expand_path("~/.booh"))
2848 fc.transient_for = $main_window
2851 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2852 push_mousecursor_wait(fc)
2853 msg = open_file_user(fc.filename)
2869 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2874 def additional_booh_options
2877 options += "--mproc #{$config['mproc'].to_i} "
2879 options += "--comments-format '#{$config['comments-format']}'"
2884 if !ask_save_modifications(utf8(_("Save this album?")),
2885 utf8(_("Do you want to save the changes to this album?")),
2886 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2889 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2891 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2892 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2893 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2895 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2896 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2897 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2898 tbl.attach(src = Gtk::Entry.new,
2899 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2900 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2901 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2902 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2903 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2904 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2905 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2906 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2907 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2908 tbl.attach(dest = Gtk::Entry.new,
2909 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2910 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2911 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2912 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2913 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2914 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2915 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2916 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2917 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2919 tooltips = Gtk::Tooltips.new
2920 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2921 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2922 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2923 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2924 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2925 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2926 tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of pictures from non digital cameras are 3/2 when pictures from digital cameras are 4/3)")), nil)
2927 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2928 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2929 nperpage_model = Gtk::ListStore.new(String, String)
2930 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
2931 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
2932 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
2933 nperpagecombo.set_attributes(crt, { :markup => 0 })
2934 iter = nperpage_model.append
2935 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
2937 [ 12, 20, 30, 40, 50 ].each { |v|
2938 iter = nperpage_model.append
2939 iter[0] = iter[1] = v.to_s
2941 nperpagecombo.active = 0
2942 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
2943 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
2944 tooltips.set_tip(indexlinkentry, utf8(_("Optional HTML markup to use on pages bottom for a small link returning to wherever you see fit in your website (or somewhere else)")), nil)
2945 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2946 pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
2947 tooltips.set_tip(madewithentry, utf8(_("Optional HTML markup to use on pages bottom for a small 'made with' label; %booh is replaced by the website of booh;\nfor example: made with <a href=%booh>booh</a>!")), nil)
2949 src_nb_calculated_for = ''
2951 process_src_nb = proc {
2952 if src.text != src_nb_calculated_for
2953 src_nb_calculated_for = src.text
2955 Thread.kill(src_nb_thread)
2958 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
2959 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
2961 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2962 if File.readable?(from_utf8_safe(src_nb_calculated_for))
2963 src_nb_thread = Thread.new {
2964 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2965 total = { 'image' => 0, 'video' => 0, nil => 0 }
2966 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2967 if File.basename(dir) =~ /^\./
2971 Dir.entries(dir.chomp).each { |file|
2972 total[entry2type(file)] += 1
2974 rescue Errno::EACCES, Errno::ENOENT
2978 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2982 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2985 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2991 timeout_src_nb = Gtk.timeout_add(100) {
2995 src_browse.signal_connect('clicked') {
2996 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2998 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3000 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3001 fc.transient_for = $main_window
3002 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3003 src.text = utf8(fc.filename)
3005 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3010 dest_browse.signal_connect('clicked') {
3011 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3013 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3015 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3016 fc.transient_for = $main_window
3017 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3018 dest.text = utf8(fc.filename)
3023 conf_browse.signal_connect('clicked') {
3024 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3026 Gtk::FileChooser::ACTION_SAVE,
3028 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3029 fc.transient_for = $main_window
3030 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3031 fc.set_current_folder(File.expand_path("~/.booh"))
3032 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3033 conf.text = utf8(fc.filename)
3040 recreate_theme_config = proc {
3041 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3043 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3044 $images_size.each { |s|
3045 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3049 tooltips.set_tip(cb, utf8(s['description']), nil)
3050 theme_sizes << { :widget => cb, :value => s['name'] }
3052 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3053 tooltips = Gtk::Tooltips.new
3054 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3055 theme_sizes << { :widget => cb, :value => 'original' }
3058 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3061 $allowed_N_values.each { |n|
3063 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3065 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3067 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3071 nperrows << { :widget => rb, :value => n }
3073 nperrowradios.show_all
3075 recreate_theme_config.call
3077 theme_button.signal_connect('clicked') {
3078 if newtheme = theme_choose(theme_button.label)
3079 theme_button.label = newtheme
3080 recreate_theme_config.call
3084 dialog.vbox.add(frame1)
3085 dialog.vbox.add(frame2)
3091 dialog.run { |response|
3092 if response == Gtk::Dialog::RESPONSE_OK
3093 srcdir = from_utf8_safe(src.text)
3094 destdir = from_utf8_safe(dest.text)
3095 confpath = from_utf8_safe(conf.text)
3096 if src.text != '' && srcdir == ''
3097 show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3099 elsif !File.directory?(srcdir)
3100 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3102 elsif dest.text != '' && destdir == ''
3103 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3105 elsif destdir != make_dest_filename(destdir)
3106 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3108 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3109 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
3111 elsif File.exists?(destdir) && !File.directory?(destdir)
3112 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3114 elsif conf.text == ''
3115 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3117 elsif conf.text != '' && confpath == ''
3118 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3120 elsif File.directory?(confpath)
3121 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3123 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3124 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3126 system("mkdir '#{destdir}'")
3127 if !File.directory?(destdir)
3128 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3140 srcdir = from_utf8(src.text)
3141 destdir = from_utf8(dest.text)
3142 configskel = File.expand_path(from_utf8(conf.text))
3143 theme = theme_button.label
3144 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3145 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3146 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3147 opt432 = optimize432.active?
3148 madewith = madewithentry.text.gsub('"', '"').gsub('\'', ''')
3149 indexlink = indexlinkentry.text.gsub('"', '"').gsub('\'', ''')
3152 Thread.kill(src_nb_thread)
3153 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3156 Gtk.timeout_remove(timeout_src_nb)
3159 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3160 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3161 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3162 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3163 utf8(_("Please wait while scanning source directory...")),
3165 { :closure_after => proc { open_file_user(configskel) } })
3170 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3172 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3173 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3174 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3176 source = $xmldoc.root.attributes['source']
3177 dest = $xmldoc.root.attributes['destination']
3178 theme = $xmldoc.root.attributes['theme']
3179 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3180 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3181 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3182 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3184 limit_sizes = limit_sizes.split(/,/)
3186 madewith = $xmldoc.root.attributes['made-with'].gsub(''', '\'')
3187 indexlink = $xmldoc.root.attributes['index-link'].gsub(''', '\'')
3189 tooltips = Gtk::Tooltips.new
3190 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3191 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3192 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3193 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3194 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3195 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3196 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3197 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3198 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3199 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3200 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3201 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3202 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3204 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3205 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3206 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3207 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3208 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3209 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3210 tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of pictures from non digital cameras are 3/2 when pictures from digital cameras are 4/3)")), nil)
3211 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3212 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3213 nperpage_model = Gtk::ListStore.new(String, String)
3214 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3215 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3216 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3217 nperpagecombo.set_attributes(crt, { :markup => 0 })
3218 iter = nperpage_model.append
3219 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3221 [ 12, 20, 30, 40, 50 ].each { |v|
3222 iter = nperpage_model.append
3223 iter[0] = iter[1] = v.to_s
3224 if nperpage && nperpage == v.to_s
3225 nperpagecombo.active_iter = iter
3228 if nperpagecombo.active_iter.nil?
3229 nperpagecombo.active = 0
3232 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3233 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3235 indexlinkentry.text = indexlink
3237 tooltips.set_tip(indexlinkentry, utf8(_("Optional HTML markup to use on pages bottom for a small link returning to wherever you see fit in your website (or somewhere else)")), nil)
3238 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3239 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3241 madewithentry.text = madewith
3243 tooltips.set_tip(madewithentry, utf8(_('Optional HTML markup to use on pages bottom for a small \'made with\' label; %booh is replaced by the website of booh;\nfor example: made with <a href=%booh>booh</a>!')), nil)
3247 recreate_theme_config = proc {
3248 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3250 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3252 $images_size.each { |s|
3253 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3255 if limit_sizes.include?(s['name'])
3263 tooltips.set_tip(cb, utf8(s['description']), nil)
3264 theme_sizes << { :widget => cb, :value => s['name'] }
3266 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3267 tooltips = Gtk::Tooltips.new
3268 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3269 if limit_sizes && limit_sizes.include?('original')
3272 theme_sizes << { :widget => cb, :value => 'original' }
3275 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3278 $allowed_N_values.each { |n|
3280 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3282 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3284 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3285 nperrowradios.add(Gtk::Label.new(' '))
3286 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3289 nperrows << { :widget => rb, :value => n.to_s }
3291 nperrowradios.show_all
3293 recreate_theme_config.call
3295 theme_button.signal_connect('clicked') {
3296 if newtheme = theme_choose(theme_button.label)
3299 theme_button.label = newtheme
3300 recreate_theme_config.call
3304 dialog.vbox.add(frame1)
3305 dialog.vbox.add(frame2)
3311 dialog.run { |response|
3312 if response == Gtk::Dialog::RESPONSE_OK
3313 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3314 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3323 save_theme = theme_button.label
3324 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3325 save_opt432 = optimize432.active?
3326 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3327 save_nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3328 save_madewith = madewithentry.text.gsub('"', '"').gsub('\'', ''')
3329 save_indexlink = indexlinkentry.text.gsub('"', '"').gsub('\'', ''')
3332 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_nperpage != nperpage || save_madewith != madewith || save_indexlink != indexlinkentry)
3333 mark_document_as_dirty
3335 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3336 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3337 (save_nperpage ? "--thumbnails-per-page #{save_nperpage} " : '') +
3338 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3339 utf8(_("Please wait while scanning source directory...")),
3341 { :closure_after => proc {
3342 open_file($filename)
3346 #- select_theme merges global variables, need to return to current choices
3347 select_current_theme
3354 sel = $albums_tv.selection.selected_rows
3356 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3357 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3358 utf8(_("Please wait while scanning source directory...")),
3360 { :closure_after => proc {
3361 open_file($filename)
3362 $albums_tv.selection.select_path(sel[0])
3370 sel = $albums_tv.selection.selected_rows
3372 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3373 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3374 utf8(_("Please wait while scanning source directory...")),
3376 { :closure_after => proc {
3377 open_file($filename)
3378 $albums_tv.selection.select_path(sel[0])
3386 theme = $xmldoc.root.attributes['theme']
3387 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3389 limit_sizes = "--sizes #{limit_sizes}"
3391 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3392 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3393 utf8(_("Please wait while scanning source directory...")),
3395 { :closure_after => proc {
3396 open_file($filename)
3402 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3404 Gtk::FileChooser::ACTION_SAVE,
3406 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3407 fc.transient_for = $main_window
3408 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3409 fc.set_current_folder(File.expand_path("~/.booh"))
3410 fc.filename = $orig_filename
3411 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3412 $orig_filename = fc.filename
3413 if ! save_current_file_user
3417 $config['last-opens'] ||= []
3418 $config['last-opens'] << $orig_filename
3424 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3426 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3427 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3428 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3430 dialog.vbox.add(notebook = Gtk::Notebook.new)
3431 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3432 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3433 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3434 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer']).set_size_request(250, -1)),
3435 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3436 tooltips = Gtk::Tooltips.new
3437 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3438 for example: /usr/bin/mplayer %f")), nil)
3439 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3440 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3441 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3442 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3443 tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3444 for example: /usr/bin/gimp-remote %f")), nil)
3445 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3446 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3447 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3448 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3449 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3450 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3451 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3452 0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3453 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(smp_hbox = Gtk::HBox.new.add(smp_spin = Gtk::SpinButton.new(2, 16, 1)).add(Gtk::Label.new(utf8(_("processors")))).set_sensitive(false)),
3454 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3455 tooltips.set_tip(smp_check, utf8(_("When activated, this option allows the thumbnails creation to run faster. However, if you don't have a multi-processor machine, this will only slow down processing!")), nil)
3456 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3457 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3458 tooltips.set_tip(nogestures_check, utf8(_("Mouse gestures are 'unusual' mouse movements triggering special actions, and are great for speeding up your editions. Get details on available mouse gestures from the Help menu.")), nil)
3459 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3460 0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3461 tooltips.set_tip(deleteondisk_check, utf8(_("Normally, deleting an image or video in booh only removes it from the web-album. If you check this option, the original file in source directory will be removed as well. Undo is possible, since actual deletion is performed only when web-album is saved.")), nil)
3463 smp_check.signal_connect('toggled') {
3464 if smp_check.active?
3465 smp_hbox.sensitive = true
3467 smp_hbox.sensitive = false
3471 smp_check.active = true
3472 smp_spin.value = $config['mproc'].to_i
3474 nogestures_check.active = $config['nogestures']
3475 deleteondisk_check.active = $config['deleteondisk']
3477 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3478 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3479 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3480 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3481 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3482 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3483 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3484 tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3485 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3486 tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3487 2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3488 tooltips.set_tip(commentsformat_entry, utf8(_("Normally, filenames without extension are used as comments for images and videos in new albums. Use this entry to use something else for images.")), nil)
3489 commentsformat_help.signal_connect('clicked') {
3490 show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3491 hence you should look at ImageMagick/identify documentation for the most
3492 accurate and up-to-date documentation. Last time I checked, documentation
3495 Print information about the image in a format of your choosing. You can
3496 include the image filename, type, width, height, Exif data, or other image