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 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., 675 Mass Ave, Cambridge, MA 02139, 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['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
112 $config['comments-format'] ||= '%t'
113 if !FileTest.directory?(File.expand_path('~/.booh'))
114 system("mkdir ~/.booh")
122 if !system("which convert >/dev/null 2>/dev/null")
123 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
124 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
127 if !system("which identify >/dev/null 2>/dev/null")
128 show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
129 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
131 missing = %w(transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
133 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
136 viewer_binary = $config['video-viewer'].split.first
137 if viewer_binary && !File.executable?(viewer_binary)
138 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
139 You should fix this in Edit/Preferences so that you can view videos.
141 Problem was: '%s' is not an executable file.
142 Hint: don't forget to specify the full path to the executable,
143 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
145 browser_binary = $config['browser'].split.first
146 if browser_binary && !File.executable?(browser_binary)
147 show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
148 You should fix this in Edit/Preferences so that you can open URLs.
150 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
155 if $config['last-opens'] && $config['last-opens'].size > 10
156 $config['last-opens'] = $config['last-opens'][-10, 10]
159 ios = File.open($config_file, "w")
160 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
161 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
162 $config.each_pair { |key, value|
163 elem = $xmldoc.root.add_element key
165 $config[key].each_pair { |subkey, subvalue|
166 subelem = elem.add_element subkey
167 subelem.add_text subvalue.to_s
169 elsif value.is_a? Array
170 elem.add_text value.join('~~~')
175 elem.add_text value.to_s
179 $xmldoc.write(ios, 0)
182 $tempfiles.each { |f|
187 def set_mousecursor(what, *widget)
188 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
189 if widget[0] && widget[0].window
190 widget[0].window.cursor = cursor
192 if $main_window && $main_window.window
193 $main_window.window.cursor = cursor
195 $current_cursor = what
197 def set_mousecursor_wait(*widget)
198 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
199 if Thread.current == Thread.main
200 Gtk.main_iteration while Gtk.events_pending?
203 def set_mousecursor_normal(*widget)
204 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
206 def push_mousecursor_wait(*widget)
207 if $current_cursor != Gdk::Cursor::WATCH
208 $save_cursor = $current_cursor
209 gtk_thread_protect { set_mousecursor_wait(*widget) }
212 def pop_mousecursor(*widget)
213 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
217 source = $xmldoc.root.attributes['source']
218 dest = $xmldoc.root.attributes['destination']
219 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
222 def full_src_dir_to_rel(path, source)
223 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
226 def build_full_dest_filename(filename)
227 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
230 def save_undo(name, closure, *params)
231 UndoHandler.save_undo(name, closure, [ *params ])
232 $undo_tb.sensitive = $undo_mb.sensitive = true
233 $redo_tb.sensitive = $redo_mb.sensitive = false
236 def view_element(filename, closures)
237 if entry2type(filename) == 'video'
238 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
244 w = Gtk::Window.new.set_title(filename)
246 msg 3, "filename: #{filename}"
247 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
248 #- typically this file won't exist in case of videos; try with the largest thumbnail around
249 if !File.exists?(dest_img)
250 if entry2type(filename) == 'video'
251 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
252 if not alternatives.empty?
253 dest_img = alternatives[-1]
256 push_mousecursor_wait
257 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
259 if !File.exists?(dest_img)
260 msg 2, _("Could not generate fullscreen thumbnail!")
265 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)))
266 evt.signal_connect('button-press-event') { |this, event|
267 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
268 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
270 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
272 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
273 delete_item.signal_connect('activate') {
275 closures[:delete].call
278 menu.popup(nil, nil, event.button, event.time)
281 evt.signal_connect('button-release-event') { |this, event|
283 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
284 msg 3, "gesture delete: click-drag right button to the bottom"
286 closures[:delete].call
287 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
291 tooltips = Gtk::Tooltips.new
292 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
294 w.signal_connect('key-press-event') { |w,event|
295 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
297 closures[:delete].call
301 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
302 b.signal_connect('clicked') { w.destroy }
305 vb.pack_start(evt, false, false)
306 vb.pack_end(bottom, false, false)
309 w.signal_connect('delete-event') { w.destroy }
310 w.window_position = Gtk::Window::POS_CENTER
314 def scroll_upper(scrolledwindow, ypos_top)
315 newval = scrolledwindow.vadjustment.value -
316 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
317 if newval < scrolledwindow.vadjustment.lower
318 newval = scrolledwindow.vadjustment.lower
320 scrolledwindow.vadjustment.value = newval
323 def scroll_lower(scrolledwindow, ypos_bottom)
324 newval = scrolledwindow.vadjustment.value +
325 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
326 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
327 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
329 scrolledwindow.vadjustment.value = newval
332 def autoscroll_if_needed(scrolledwindow, image, textview)
333 #- autoscroll if cursor or image is not visible, if possible
334 if image && image.window || textview.window
335 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
336 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
337 current_miny_visible = scrolledwindow.vadjustment.value
338 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
339 if ypos_top < current_miny_visible
340 scroll_upper(scrolledwindow, ypos_top)
341 elsif ypos_bottom > current_maxy_visible
342 scroll_lower(scrolledwindow, ypos_bottom)
347 def create_editzone(scrolledwindow, pagenum, image)
348 frame = Gtk::Frame.new
349 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
350 frame.set_shadow_type(Gtk::SHADOW_IN)
351 textview.signal_connect('key-press-event') { |w, event|
352 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
353 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
354 scrolledwindow.signal_emit('key-press-event', event)
356 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
357 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
358 if event.keyval == Gdk::Keyval::GDK_Up
359 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
360 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
362 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
365 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
366 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
368 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
375 candidate_undo_text = nil
376 textview.signal_connect('focus-in-event') { |w, event|
377 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
378 candidate_undo_text = textview.buffer.text
382 textview.signal_connect('key-release-event') { |w, event|
383 if candidate_undo_text && candidate_undo_text != textview.buffer.text
385 save_undo(_("text edit"),
387 save_text = textview.buffer.text
388 textview.buffer.text = text
390 $notebook.set_page(pagenum)
392 textview.buffer.text = save_text
394 $notebook.set_page(pagenum)
396 }, candidate_undo_text)
397 candidate_undo_text = nil
400 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)
401 autoscroll_if_needed(scrolledwindow, image, textview)
406 return [ frame, textview ]
409 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
411 if !$modified_pixbufs[thumbnail_img]
412 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
413 elsif !$modified_pixbufs[thumbnail_img][:orig]
414 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
417 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
420 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
421 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
422 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
423 if pixbuf.height > desired_y
424 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
425 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
426 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
431 if $modified_pixbufs[thumbnail_img][:whitebalance]
432 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
435 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
438 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
441 #- update rotate attribute
442 xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
444 $modified_pixbufs[thumbnail_img] ||= {}
445 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
446 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
448 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
451 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
454 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
456 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
458 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
459 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
461 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
462 $notebook.set_page(0)
463 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
468 def color_swap(xmldir, attributes_prefix)
470 if xmldir.attributes["#{attributes_prefix}color-swap"]
471 xmldir.delete_attribute("#{attributes_prefix}color-swap")
473 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
477 def enhance(xmldir, attributes_prefix)
479 if xmldir.attributes["#{attributes_prefix}enhance"]
480 xmldir.delete_attribute("#{attributes_prefix}enhance")
482 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
486 def change_frame_offset(xmldir, attributes_prefix, value)
488 xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
491 def ask_new_frame_offset(xmldir, attributes_prefix)
493 value = xmldir.attributes["#{attributes_prefix}frame-offset"]
498 dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
500 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
501 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
502 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
506 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
507 from. There are approximately 25 frames per second in a video.
510 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
511 entry.signal_connect('key-press-event') { |w, event|
512 if event.keyval == Gdk::Keyval::GDK_Return
513 dialog.response(Gtk::Dialog::RESPONSE_OK)
515 elsif event.keyval == Gdk::Keyval::GDK_Escape
516 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
519 false #- propagate if needed
523 dialog.window_position = Gtk::Window::POS_MOUSE
526 dialog.run { |response|
529 if response == Gtk::Dialog::RESPONSE_OK
531 msg 3, "changing frame offset to #{newval}"
532 return { :old => value, :new => newval }
539 def change_pano_amount(xmldir, attributes_prefix, value)
542 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
544 xmldir.add_attribute("#{attributes_prefix}pano-amount", value)
548 def ask_new_pano_amount(xmldir, attributes_prefix)
550 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
555 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
557 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
558 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
559 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
563 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
564 of this panorama image compared to other regular images. For example, if the panorama
565 was taken out of four photos on one row, counting the necessary overlap, the width of
566 this panorama image should probably be roughly three times the width of regular images.
568 With this information, booh will be able to generate panorama thumbnails looking
572 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)")))).
573 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
574 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
575 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
576 dialog.window_position = Gtk::Window::POS_MOUSE
579 spin.value = value.to_f
586 dialog.run { |response|
590 newval = spin.value.to_f
593 if response == Gtk::Dialog::RESPONSE_OK
595 msg 3, "changing panorama amount to #{newval}"
596 return { :old => value, :new => newval }
603 def change_whitebalance(xmlelem, attributes_prefix, value)
605 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
608 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
610 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
611 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
612 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
613 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
614 destfile = "#{thumbnail_img}-orig-whitebalance.jpg"
615 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
616 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
617 $modified_pixbufs[thumbnail_img] ||= {}
618 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
619 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
620 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
623 $modified_pixbufs[thumbnail_img] ||= {}
624 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
626 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
629 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
630 #- init $modified_pixbufs correctly
631 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
633 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
635 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
637 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
638 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
639 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
643 _("You can fix the <b>white balance</b> of the image, if your image is too blue
644 or too yellow because your camera didn't detect the light correctly. Drag the
645 slider below the image to the left for more blue, to the right for more yellow.
649 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
651 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
653 dialog.window_position = Gtk::Window::POS_MOUSE
657 timeout = Gtk.timeout_add(100) {
658 if hs.value != lastval
661 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
667 dialog.run { |response|
668 Gtk.timeout_remove(timeout)
669 if response == Gtk::Dialog::RESPONSE_OK
671 newval = hs.value.to_s
672 msg 3, "changing white balance to #{newval}"
674 return { :old => value, :new => newval }
677 $modified_pixbufs[thumbnail_img] ||= {}
678 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
679 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
687 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
688 system("rm -f '#{destfile}'")
689 #- type can be 'element' or 'subdir'
691 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
693 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
697 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
699 push_mousecursor_wait
700 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
703 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
709 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
710 distribute_multiple_call = Proc.new { |action, arg|
711 $selected_elements.each_key { |path|
712 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
714 if possible_actions[:can_multiple] && $selected_elements.length > 0
715 UndoHandler.begin_batch
716 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
717 UndoHandler.end_batch
719 closures[action].call(arg)
721 $selected_elements = {}
724 if optionals.include?('change_image')
725 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
726 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
727 changeimg.signal_connect('activate') { closures[:change].call }
728 menu.append(Gtk::SeparatorMenuItem.new)
730 if !possible_actions[:can_multiple] || $selected_elements.length == 0
733 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
734 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
735 view.signal_connect('activate') { closures[:view].call }
737 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
738 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
739 view.signal_connect('activate') { closures[:view].call }
740 menu.append(Gtk::SeparatorMenuItem.new)
743 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
744 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
745 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
746 exif.signal_connect('activate') { show_popup($main_window,
747 utf8(`identify -format "%[EXIF:*]" #{fullpath}`.sub(/MakerNote.*\n/, '')),
748 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
749 menu.append(Gtk::SeparatorMenuItem.new)
752 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
753 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
754 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
755 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
756 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
757 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
758 if !possible_actions[:can_multiple] || $selected_elements.length == 0
759 menu.append(Gtk::SeparatorMenuItem.new)
760 if !possible_actions[:forbid_left]
761 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
762 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
763 moveleft.signal_connect('activate') { closures[:move].call('left') }
764 if !possible_actions[:can_left]
765 moveleft.sensitive = false
768 if !possible_actions[:forbid_right]
769 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
770 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
771 moveright.signal_connect('activate') { closures[:move].call('right') }
772 if !possible_actions[:can_right]
773 moveright.sensitive = false
776 if optionals.include?('move_top')
777 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
778 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
779 movetop.signal_connect('activate') { closures[:move].call('top') }
780 if !possible_actions[:can_top]
781 movetop.sensitive = false
784 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
785 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
786 moveup.signal_connect('activate') { closures[:move].call('up') }
787 if !possible_actions[:can_up]
788 moveup.sensitive = false
790 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
791 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
792 movedown.signal_connect('activate') { closures[:move].call('down') }
793 if !possible_actions[:can_down]
794 movedown.sensitive = false
796 if optionals.include?('move_bottom')
797 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
798 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
799 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
800 if !possible_actions[:can_bottom]
801 movebottom.sensitive = false
806 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
807 menu.append(Gtk::SeparatorMenuItem.new)
808 menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
809 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
810 color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
811 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
812 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
813 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
814 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
815 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
816 frame_offset.signal_connect('activate') {
817 if possible_actions[:can_multiple] && $selected_elements.length > 0
818 if values = ask_new_frame_offset(nil, '')
819 distribute_multiple_call.call(:frame_offset, values)
822 closures[:frame_offset].call
827 menu.append( Gtk::SeparatorMenuItem.new)
828 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
829 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
830 whitebalance.signal_connect('activate') {
831 if possible_actions[:can_multiple] && $selected_elements.length > 0
832 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
833 distribute_multiple_call.call(:whitebalance, values)
836 closures[:whitebalance].call
839 if !possible_actions[:can_multiple] || $selected_elements.length == 0
840 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
841 _("Enhance constrast"))))
843 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
845 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
846 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
847 if type == 'image' && possible_actions[:can_panorama]
848 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
849 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
850 panorama.signal_connect('activate') {
851 if possible_actions[:can_multiple] && $selected_elements.length > 0
852 if values = ask_new_pano_amount(nil, '')
853 distribute_multiple_call.call(:pano, values)
856 distribute_multiple_call.call(:pano)
860 if optionals.include?('delete')
861 menu.append( Gtk::SeparatorMenuItem.new)
862 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
863 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
864 if !possible_actions[:can_multiple] || $selected_elements.length == 0
865 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
866 paste_item.signal_connect('activate') { closures[:paste].call }
867 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
868 clear_item.signal_connect('activate') { $cuts = [] }
870 paste_item.sensitive = clear_item.sensitive = false
873 menu.append( Gtk::SeparatorMenuItem.new)
874 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
875 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
878 menu.popup(nil, nil, event.button, event.time)
881 def delete_current_subalbum
883 sel = $albums_tv.selection.selected_rows
884 $xmldir.elements.each { |e|
885 if e.name == 'image' || e.name == 'video'
886 e.add_attribute('deleted', 'true')
889 #- branch if we have a non deleted subalbum
890 if $xmldir.child_byname_notattr('dir', 'deleted')
891 $xmldir.delete_attribute('thumbnails-caption')
892 $xmldir.delete_attribute('thumbnails-captionfile')
894 $xmldir.add_attribute('deleted', 'true')
896 while moveup.parent.name == 'dir'
897 moveup = moveup.parent
898 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
899 moveup.add_attribute('deleted', 'true')
906 save_changes('forced')
907 populate_subalbums_treeview(false)
908 $albums_tv.selection.select_path(sel[0])
914 $current_path = nil #- prevent save_changes from being rerun again
915 sel = $albums_tv.selection.selected_rows
916 restore_one = proc { |xmldir|
917 xmldir.elements.each { |e|
918 if e.name == 'dir' && e.attributes['deleted']
921 e.delete_attribute('deleted')
924 restore_one.call($xmldir)
925 populate_subalbums_treeview(false)
926 $albums_tv.selection.select_path(sel[0])
929 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
932 frame1 = Gtk::Frame.new
933 fullpath = from_utf8("#{$current_path}/#{filename}")
935 my_gen_real_thumbnail = proc {
936 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
940 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
941 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
942 pack_start(img = Gtk::Image.new).
943 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
944 px, mask = pxb.render_pixmap_and_mask
945 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
946 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
948 frame1.add(img = Gtk::Image.new)
951 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
952 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
953 my_gen_real_thumbnail.call
955 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
958 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
960 tooltips = Gtk::Tooltips.new
961 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
962 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
964 frame2, textview = create_editzone($autotable_sw, 1, img)
965 textview.buffer.text = caption
966 textview.set_justification(Gtk::Justification::CENTER)
968 vbox = Gtk::VBox.new(false, 5)
969 vbox.pack_start(evtbox, false, false)
970 vbox.pack_start(frame2, false, false)
971 autotable.append(vbox, filename)
973 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
974 $vbox2widgets[vbox] = { :textview => textview, :image => img }
976 #- to be able to find widgets by name
977 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
979 cleanup_all_thumbnails = proc {
980 #- remove out of sync images
981 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
982 for sizeobj in $images_size
983 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
988 rotate_and_cleanup = proc { |angle|
989 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
990 cleanup_all_thumbnails.call
993 move = proc { |direction|
994 do_method = "move_#{direction}"
995 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
997 done = autotable.method(do_method).call(vbox)
998 textview.grab_focus #- because if moving, focus is stolen
1002 save_undo(_("move %s") % direction,
1004 autotable.method(undo_method).call(vbox)
1005 textview.grab_focus #- because if moving, focus is stolen
1006 autoscroll_if_needed($autotable_sw, img, textview)
1007 $notebook.set_page(1)
1009 autotable.method(do_method).call(vbox)
1010 textview.grab_focus #- because if moving, focus is stolen
1011 autoscroll_if_needed($autotable_sw, img, textview)
1012 $notebook.set_page(1)
1018 color_swap_and_cleanup = proc {
1019 perform_color_swap_and_cleanup = proc {
1020 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1021 my_gen_real_thumbnail.call
1024 cleanup_all_thumbnails.call
1025 perform_color_swap_and_cleanup.call
1027 save_undo(_("color swap"),
1029 perform_color_swap_and_cleanup.call
1031 autoscroll_if_needed($autotable_sw, img, textview)
1032 $notebook.set_page(1)
1034 perform_color_swap_and_cleanup.call
1036 autoscroll_if_needed($autotable_sw, img, textview)
1037 $notebook.set_page(1)
1042 change_frame_offset_and_cleanup_real = proc { |values|
1043 perform_change_frame_offset_and_cleanup = proc { |val|
1044 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
1045 my_gen_real_thumbnail.call
1047 perform_change_frame_offset_and_cleanup.call(values[:new])
1049 save_undo(_("specify frame offset"),
1051 perform_change_frame_offset_and_cleanup.call(values[:old])
1053 autoscroll_if_needed($autotable_sw, img, textview)
1054 $notebook.set_page(1)
1056 perform_change_frame_offset_and_cleanup.call(values[:new])
1058 autoscroll_if_needed($autotable_sw, img, textview)
1059 $notebook.set_page(1)
1064 change_frame_offset_and_cleanup = proc {
1065 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
1066 change_frame_offset_and_cleanup_real.call(values)
1070 change_pano_amount_and_cleanup_real = proc { |values|
1071 perform_change_pano_amount_and_cleanup = proc { |val|
1072 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1074 perform_change_pano_amount_and_cleanup.call(values[:new])
1076 save_undo(_("change panorama amount"),
1078 perform_change_pano_amount_and_cleanup.call(values[:old])
1080 autoscroll_if_needed($autotable_sw, img, textview)
1081 $notebook.set_page(1)
1083 perform_change_pano_amount_and_cleanup.call(values[:new])
1085 autoscroll_if_needed($autotable_sw, img, textview)
1086 $notebook.set_page(1)
1091 change_pano_amount_and_cleanup = proc {
1092 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1093 change_pano_amount_and_cleanup_real.call(values)
1097 whitebalance_and_cleanup_real = proc { |values|
1098 perform_change_whitebalance_and_cleanup = proc { |val|
1099 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1100 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1101 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1102 cleanup_all_thumbnails.call
1104 perform_change_whitebalance_and_cleanup.call(values[:new])
1106 save_undo(_("fix white balance"),
1108 perform_change_whitebalance_and_cleanup.call(values[:old])
1110 autoscroll_if_needed($autotable_sw, img, textview)
1111 $notebook.set_page(1)
1113 perform_change_whitebalance_and_cleanup.call(values[:new])
1115 autoscroll_if_needed($autotable_sw, img, textview)
1116 $notebook.set_page(1)
1121 whitebalance_and_cleanup = proc {
1122 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1123 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1124 whitebalance_and_cleanup_real.call(values)
1128 enhance_and_cleanup = proc {
1129 perform_enhance_and_cleanup = proc {
1130 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1131 my_gen_real_thumbnail.call
1134 cleanup_all_thumbnails.call
1135 perform_enhance_and_cleanup.call
1137 save_undo(_("enhance"),
1139 perform_enhance_and_cleanup.call
1141 autoscroll_if_needed($autotable_sw, img, textview)
1142 $notebook.set_page(1)
1144 perform_enhance_and_cleanup.call
1146 autoscroll_if_needed($autotable_sw, img, textview)
1147 $notebook.set_page(1)
1152 delete = proc { |isacut|
1153 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 })
1156 perform_delete = proc {
1157 after = autotable.get_next_widget(vbox)
1159 after = autotable.get_previous_widget(vbox)
1161 if $config['deleteondisk'] && !isacut
1162 msg 3, "scheduling for delete: #{fullpath}"
1163 $todelete << fullpath
1165 autotable.remove(vbox)
1167 $vbox2widgets[after][:textview].grab_focus
1168 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1172 previous_pos = autotable.get_current_number(vbox)
1176 delete_current_subalbum
1178 save_undo(_("delete"),
1180 autotable.reinsert(pos, vbox, filename)
1181 $notebook.set_page(1)
1182 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1184 msg 3, "removing deletion schedule of: #{fullpath}"
1185 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1188 $notebook.set_page(1)
1197 $cuts << { :vbox => vbox, :filename => filename }
1198 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1203 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1206 autotable.queue_draws << proc {
1207 $vbox2widgets[last[:vbox]][:textview].grab_focus
1208 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1210 save_undo(_("paste"),
1212 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1213 $notebook.set_page(1)
1216 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1218 $notebook.set_page(1)
1221 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1226 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1227 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1228 :whitebalance => whitebalance_and_cleanup_real, :pano => change_pano_amount_and_cleanup_real }
1230 textview.signal_connect('key-press-event') { |w, event|
1233 x, y = autotable.get_current_pos(vbox)
1234 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1235 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1236 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1237 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1239 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1240 $vbox2widgets[widget_up][:textview].grab_focus
1247 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1249 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1250 $vbox2widgets[widget_down][:textview].grab_focus
1257 if event.keyval == Gdk::Keyval::GDK_Left
1260 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1267 rotate_and_cleanup.call(-90)
1270 if event.keyval == Gdk::Keyval::GDK_Right
1271 next_ = autotable.get_next_widget(vbox)
1272 if next_ && autotable.get_current_pos(next_)[0] > x
1274 $vbox2widgets[next_][:textview].grab_focus
1281 rotate_and_cleanup.call(90)
1284 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1287 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1288 view_element(filename, { :delete => delete })
1291 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1294 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1298 !propagate #- propagate if needed
1301 $ignore_next_release = false
1302 evtbox.signal_connect('button-press-event') { |w, event|
1303 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1304 if event.state & Gdk::Window::BUTTON3_MASK != 0
1305 #- gesture redo: hold right mouse button then click left mouse button
1306 $config['nogestures'] or perform_redo
1307 $ignore_next_release = true
1309 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1311 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1313 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1314 elsif $enhance.active?
1315 enhance_and_cleanup.call
1316 elsif $delete.active?
1320 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1323 $button1_pressed_autotable = true
1324 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1325 if event.state & Gdk::Window::BUTTON1_MASK != 0
1326 #- gesture undo: hold left mouse button then click right mouse button
1327 $config['nogestures'] or perform_undo
1328 $ignore_next_release = true
1330 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1331 view_element(filename, { :delete => delete })
1336 evtbox.signal_connect('button-release-event') { |w, event|
1337 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1338 if !$ignore_next_release
1339 x, y = autotable.get_current_pos(vbox)
1340 next_ = autotable.get_next_widget(vbox)
1341 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1342 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1343 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1344 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1345 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1346 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1347 :pano => change_pano_amount_and_cleanup })
1349 $ignore_next_release = false
1350 $gesture_press = nil
1355 #- handle reordering with drag and drop
1356 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1357 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1358 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1359 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1362 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1364 #- mouse gesture first (dnd disables button-release-event)
1365 if $gesture_press && $gesture_press[:filename] == filename
1366 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1367 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1368 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1369 rotate_and_cleanup.call(angle)
1370 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1372 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1373 msg 3, "gesture delete: click-drag right button to the bottom"
1375 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1380 ctxt.targets.each { |target|
1381 if target.name == 'reorder-elements'
1382 move_dnd = proc { |from,to|
1385 autotable.move(from, to)
1386 save_undo(_("reorder"),
1389 autotable.move(to - 1, from)
1391 autotable.move(to, from + 1)
1393 $notebook.set_page(1)
1395 autotable.move(from, to)
1396 $notebook.set_page(1)
1401 if $multiple_dnd.size == 0
1402 move_dnd.call(selection_data.data.to_i,
1403 autotable.get_current_number(vbox))
1405 UndoHandler.begin_batch
1406 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1408 #- need to update current position between each call
1409 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1410 autotable.get_current_number(vbox))
1412 UndoHandler.end_batch
1423 def create_auto_table
1425 $autotable = Gtk::AutoTable.new(5)
1427 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1428 thumbnails_vb = Gtk::VBox.new(false, 5)
1430 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1431 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1432 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1433 thumbnails_vb.add($autotable)
1435 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1436 $autotable_sw.add_with_viewport(thumbnails_vb)
1438 #- follows stuff for handling multiple elements selection
1439 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1441 update_selected = proc {
1442 $autotable.current_order.each { |path|
1443 w = $name2widgets[path][:evtbox].window
1444 xm = w.position[0] + w.size[0]/2
1445 ym = w.position[1] + w.size[1]/2
1446 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1447 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1448 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1449 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1452 if $selected_elements[path] && ! $selected_elements[path][:keep]
1453 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))
1454 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1455 $selected_elements.delete(path)
1460 $autotable.signal_connect('realize') { |w,e|
1461 gc = Gdk::GC.new($autotable.window)
1462 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1463 gc.function = Gdk::GC::INVERT
1464 #- autoscroll handling for DND and multiple selections
1465 Gtk.timeout_add(100) {
1466 if ! $autotable.window.nil?
1467 w, x, y, mask = $autotable.window.pointer
1468 if mask & Gdk::Window::BUTTON1_MASK != 0
1469 if y < $autotable_sw.vadjustment.value
1471 $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]])
1473 if $button1_pressed_autotable || press_x
1474 scroll_upper($autotable_sw, y)
1477 w, pos_x, pos_y = $autotable.window.pointer
1478 $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]])
1479 update_selected.call
1482 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1484 $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]])
1486 if $button1_pressed_autotable || press_x
1487 scroll_lower($autotable_sw, y)
1490 w, pos_x, pos_y = $autotable.window.pointer
1491 $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]])
1492 update_selected.call
1497 ! $autotable.window.nil?
1501 $autotable.signal_connect('button-press-event') { |w,e|
1503 if !$button1_pressed_autotable
1506 if e.state & Gdk::Window::SHIFT_MASK == 0
1507 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1508 $selected_elements = {}
1509 $statusbar.push(0, utf8(_("Nothing selected.")))
1511 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1513 set_mousecursor(Gdk::Cursor::TCROSS)
1517 $autotable.signal_connect('button-release-event') { |w,e|
1519 if $button1_pressed_autotable
1520 #- unselect all only now
1521 $multiple_dnd = $selected_elements.keys
1522 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1523 $selected_elements = {}
1524 $button1_pressed_autotable = false
1527 $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]])
1528 if $selected_elements.length > 0
1529 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1532 press_x = press_y = pos_x = pos_y = nil
1533 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1537 $autotable.signal_connect('motion-notify-event') { |w,e|
1540 $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]])
1544 $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]])
1545 update_selected.call
1551 def create_subalbums_page
1553 subalbums_hb = Gtk::HBox.new
1554 $subalbums_vb = Gtk::VBox.new(false, 5)
1555 subalbums_hb.pack_start($subalbums_vb, false, false)
1556 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1557 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1558 $subalbums_sw.add_with_viewport(subalbums_hb)
1561 def save_current_file
1567 ios = File.open($filename, "w")
1568 $xmldoc.write(ios, 0)
1570 rescue Iconv::IllegalSequence
1571 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1572 if ! ios.nil? && ! ios.closed?
1575 $xmldoc.xml_decl.encoding = 'UTF-8'
1576 ios = File.open($filename, "w")
1577 $xmldoc.write(ios, 0)
1587 def save_current_file_user
1588 save_tempfilename = $filename
1589 $filename = $orig_filename
1590 if ! save_current_file
1591 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1592 $filename = save_tempfilename
1596 $generated_outofline = false
1597 $filename = save_tempfilename
1599 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1600 $todelete.each { |f|
1601 system("rm -f #{f}")
1605 def mark_document_as_dirty
1606 $xmldoc.elements.each('//dir') { |elem|
1607 elem.delete_attribute('already-generated')
1611 #- ret: true => ok false => cancel
1612 def ask_save_modifications(msg1, msg2, *options)
1614 options = options.size > 0 ? options[0] : {}
1616 if options[:disallow_cancel]
1617 dialog = Gtk::Dialog.new(msg1,
1619 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1620 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1621 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1623 dialog = Gtk::Dialog.new(msg1,
1625 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1626 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1627 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1628 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1630 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1631 dialog.vbox.add(Gtk::Label.new(msg2))
1632 dialog.window_position = Gtk::Window::POS_CENTER
1635 dialog.run { |response|
1637 if response == Gtk::Dialog::RESPONSE_YES
1638 if ! save_current_file_user
1639 return ask_save_modifications(msg1, msg2, options)
1642 #- if we have generated an album but won't save modifications, we must remove
1643 #- already-generated markers in original file
1644 if $generated_outofline
1646 $xmldoc = REXML::Document.new File.new($orig_filename)
1647 mark_document_as_dirty
1648 ios = File.open($orig_filename, "w")
1649 $xmldoc.write(ios, 0)
1652 puts "exception: #{$!}"
1656 if response == Gtk::Dialog::RESPONSE_CANCEL
1659 $todelete = [] #- unconditionally clear the list of images/videos to delete
1665 def try_quit(*options)
1666 if ask_save_modifications(utf8(_("Save before quitting?")),
1667 utf8(_("Do you want to save your changes before quitting?")),
1673 def show_popup(parent, msg, *options)
1674 dialog = Gtk::Dialog.new
1675 if options[0] && options[0][:title]
1676 dialog.title = options[0][:title]
1678 dialog.title = utf8(_("Booh message"))
1680 lbl = Gtk::Label.new
1681 if options[0] && options[0][:nomarkup]
1686 if options[0] && options[0][:centered]
1687 lbl.set_justify(Gtk::Justification::CENTER)
1689 if options[0] && options[0][:selectable]
1690 lbl.selectable = true
1692 if options[0] && options[0][:topwidget]
1693 dialog.vbox.add(options[0][:topwidget])
1695 if options[0] && options[0][:scrolled]
1696 sw = Gtk::ScrolledWindow.new(nil, nil)
1697 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1698 sw.add_with_viewport(lbl)
1700 dialog.set_default_size(500, 600)
1702 dialog.vbox.add(lbl)
1703 dialog.set_default_size(200, 120)
1705 if options[0] && options[0][:okcancel]
1706 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1708 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1710 if options[0] && options[0][:pos_centered]
1711 dialog.window_position = Gtk::Window::POS_CENTER
1713 dialog.window_position = Gtk::Window::POS_MOUSE
1716 if options[0] && options[0][:linkurl]
1717 linkbut = Gtk::Button.new('')
1718 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1719 linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1720 linkbut.relief = Gtk::RELIEF_NONE
1721 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1722 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1723 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1728 if !options[0] || !options[0][:not_transient]
1729 dialog.transient_for = parent
1730 dialog.run { |response|
1732 if options[0] && options[0][:okcancel]
1733 return response == Gtk::Dialog::RESPONSE_OK
1737 dialog.signal_connect('response') { dialog.destroy }
1741 def backend_wait_message(parent, msg, infopipe_path, mode)
1743 w.set_transient_for(parent)
1746 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1747 vb.pack_start(Gtk::Label.new(msg), false, false)
1749 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1750 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1751 if mode != 'one dir scan'
1752 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1754 if mode == 'web-album'
1755 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1756 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1758 vb.pack_start(Gtk::HSeparator.new, false, false)
1760 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1761 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1762 vb.pack_end(bottom, false, false)
1764 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1765 refresh_thread = Thread.new {
1766 directories_counter = 0
1767 while line = infopipe.gets
1768 if line =~ /^directories: (\d+), sizes: (\d+)/
1769 directories = $1.to_f + 1
1771 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1772 elements = $3.to_f + 1
1773 if mode == 'web-album'
1777 gtk_thread_protect { pb1_1.fraction = 0 }
1778 if mode != 'one dir scan'
1779 newtext = utf8(full_src_dir_to_rel($1, $2))
1780 newtext = '/' if newtext == ''
1781 gtk_thread_protect { pb1_2.text = newtext }
1782 directories_counter += 1
1783 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1785 elsif line =~ /^processing element$/
1786 element_counter += 1
1787 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1788 elsif line =~ /^processing size$/
1789 element_counter += 1
1790 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1791 elsif line =~ /^finished processing sizes$/
1792 gtk_thread_protect { pb1_1.fraction = 1 }
1793 elsif line =~ /^creating index.html$/
1794 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1795 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1796 directories_counter = 0
1797 elsif line =~ /^index.html: (.+)\|(.+)/
1798 newtext = utf8(full_src_dir_to_rel($1, $2))
1799 newtext = '/' if newtext == ''
1800 gtk_thread_protect { pb2.text = newtext }
1801 directories_counter += 1
1802 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1803 elsif line =~ /^die: (.*)$/
1810 w.signal_connect('delete-event') { w.destroy }
1811 w.signal_connect('destroy') {
1812 Thread.kill(refresh_thread)
1813 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1816 system("rm -f #{infopipe_path}")
1819 w.window_position = Gtk::Window::POS_CENTER
1825 def call_backend(cmd, waitmsg, mode, params)
1826 pipe = Tempfile.new("boohpipe")
1828 system("mkfifo #{pipe.path}")
1829 cmd += " --info-pipe #{pipe.path}"
1830 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
1835 id, exitstatus = Process.waitpid2(pid)
1836 gtk_thread_protect { w8.destroy }
1838 if params[:successmsg]
1839 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
1841 if params[:closure_after]
1842 gtk_thread_protect(¶ms[:closure_after])
1844 elsif exitstatus == 15
1845 #- say nothing, user aborted
1847 gtk_thread_protect { show_popup($main_window,
1848 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
1854 button.signal_connect('clicked') {
1855 Process.kill('SIGTERM', pid)
1859 def save_changes(*forced)
1860 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1864 $xmldir.delete_attribute('already-generated')
1866 propagate_children = proc { |xmldir|
1867 if xmldir.attributes['subdirs-caption']
1868 xmldir.delete_attribute('already-generated')
1870 xmldir.elements.each('dir') { |element|
1871 propagate_children.call(element)
1875 if $xmldir.child_byname_notattr('dir', 'deleted')
1876 new_title = $subalbums_title.buffer.text
1877 if new_title != $xmldir.attributes['subdirs-caption']
1878 parent = $xmldir.parent
1879 if parent.name == 'dir'
1880 parent.delete_attribute('already-generated')
1882 propagate_children.call($xmldir)
1884 $xmldir.add_attribute('subdirs-caption', new_title)
1885 $xmldir.elements.each('dir') { |element|
1886 if !element.attributes['deleted']
1887 path = element.attributes['path']
1888 newtext = $subalbums_edits[path][:editzone].buffer.text
1889 if element.attributes['subdirs-caption']
1890 if element.attributes['subdirs-caption'] != newtext
1891 propagate_children.call(element)
1893 element.add_attribute('subdirs-caption', newtext)
1894 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1896 if element.attributes['thumbnails-caption'] != newtext
1897 element.delete_attribute('already-generated')
1899 element.add_attribute('thumbnails-caption', newtext)
1900 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1906 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
1907 if $xmldir.attributes['thumbnails-caption']
1908 path = $xmldir.attributes['path']
1909 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1911 elsif $xmldir.attributes['thumbnails-caption']
1912 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
1915 #- remove and reinsert elements to reflect new ordering
1918 $xmldir.elements.each { |element|
1919 if element.name == 'image' || element.name == 'video'
1920 saves[element.attributes['filename']] = element.remove
1924 $autotable.current_order.each { |path|
1925 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1926 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1929 saves.each_key { |path|
1930 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1931 chld.add_attribute('deleted', 'true')
1935 def sort_by_exif_date
1939 $xmldir.elements.each { |element|
1940 if element.name == 'image' || element.name == 'video'
1941 current_order << element.attributes['filename']
1945 #- look for EXIF dates
1947 w.set_transient_for($main_window)
1949 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1950 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
1951 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
1952 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1953 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1954 vb.pack_end(bottom, false, false)
1956 w.signal_connect('delete-event') { w.destroy }
1957 w.window_position = Gtk::Window::POS_CENTER
1961 b.signal_connect('clicked') { aborted = true }
1964 current_order.each { |f|
1966 if entry2type(f) == 'image'
1968 pb.fraction = i.to_f / current_order.size
1969 Gtk.main_iteration while Gtk.events_pending?
1970 date_time = `identify -format "%[EXIF:DateTime]" '#{from_utf8($current_path + "/" + f)}'`.chomp
1971 if $? == 0 && date_time != ''
1972 dates[f] = date_time
1985 $xmldir.elements.each { |element|
1986 if element.name == 'image' || element.name == 'video'
1987 saves[element.attributes['filename']] = element.remove
1991 #- find a good fallback for all entries without a date (still next to the item they were next to)
1992 neworder = dates.keys.sort { |a,b| dates[a] <=> dates[b] }
1993 for i in 0 .. current_order.size - 1
1994 if ! neworder.include?(current_order[i])
1996 while j > 0 && ! neworder.include?(current_order[j])
1999 neworder[(neworder.index(current_order[j]) || -1 ) + 1, 0] = current_order[i]
2003 $xmldir.add_element(saves[f].name, saves[f].attributes)
2006 #- let the auto-table reflect new ordering
2010 def remove_all_captions
2013 $autotable.current_order.each { |path|
2014 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2015 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2017 save_undo(_("remove all captions"),
2019 texts.each_key { |key|
2020 $name2widgets[key][:textview].buffer.text = texts[key]
2022 $notebook.set_page(1)
2024 texts.each_key { |key|
2025 $name2widgets[key][:textview].buffer.text = ''
2027 $notebook.set_page(1)
2033 $selected_elements.each_key { |path|
2034 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2040 $selected_elements = {}
2044 $undo_tb.sensitive = $undo_mb.sensitive = false
2045 $redo_tb.sensitive = $redo_mb.sensitive = false
2051 $subalbums_vb.children.each { |chld|
2052 $subalbums_vb.remove(chld)
2054 $subalbums = Gtk::Table.new(0, 0, true)
2055 current_y_sub_albums = 0
2057 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2058 $subalbums_edits = {}
2059 subalbums_counter = 0
2060 subalbums_edits_bypos = {}
2062 add_subalbum = proc { |xmldir, counter|
2063 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2064 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2065 if xmldir == $xmldir
2066 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2067 caption = xmldir.attributes['thumbnails-caption']
2068 captionfile, dummy = find_subalbum_caption_info(xmldir)
2069 infotype = 'thumbnails'
2071 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2072 captionfile, caption = find_subalbum_caption_info(xmldir)
2073 infotype = find_subalbum_info_type(xmldir)
2075 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2076 hbox = Gtk::HBox.new
2077 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2079 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2082 my_gen_real_thumbnail = proc {
2083 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2086 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2087 f.add(img = Gtk::Image.new)
2088 my_gen_real_thumbnail.call
2090 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2092 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2093 $subalbums.attach(hbox,
2094 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2096 frame, textview = create_editzone($subalbums_sw, 0, img)
2097 textview.buffer.text = caption
2098 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2099 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2101 change_image = proc {
2102 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2104 Gtk::FileChooser::ACTION_OPEN,
2106 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2107 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2108 fc.transient_for = $main_window
2109 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))
2110 f.add(preview_img = Gtk::Image.new)
2112 fc.signal_connect('update-preview') { |w|
2114 if fc.preview_filename
2115 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2116 fc.preview_widget_active = true
2118 rescue Gdk::PixbufError
2119 fc.preview_widget_active = false
2122 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2124 old_file = captionfile
2125 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2126 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2127 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2128 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2130 new_file = fc.filename
2131 msg 3, "new captionfile is: #{fc.filename}"
2132 perform_changefile = proc {
2133 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2134 $modified_pixbufs.delete(thumbnail_file)
2135 xmldir.delete_attribute("#{infotype}-rotate")
2136 xmldir.delete_attribute("#{infotype}-color-swap")
2137 xmldir.delete_attribute("#{infotype}-enhance")
2138 xmldir.delete_attribute("#{infotype}-frame-offset")
2139 my_gen_real_thumbnail.call
2141 perform_changefile.call
2143 save_undo(_("change caption file for sub-album"),
2145 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2146 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2147 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2148 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2149 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2150 my_gen_real_thumbnail.call
2151 $notebook.set_page(0)
2153 perform_changefile.call
2154 $notebook.set_page(0)
2161 rotate_and_cleanup = proc { |angle|
2162 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2163 system("rm -f '#{thumbnail_file}'")
2166 move = proc { |direction|
2169 save_changes('forced')
2170 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2171 if direction == 'up'
2172 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2173 subalbums_edits_bypos[oldpos - 1][:position] += 1
2175 if direction == 'down'
2176 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2177 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2179 if direction == 'top'
2180 for i in 1 .. oldpos - 1
2181 subalbums_edits_bypos[i][:position] += 1
2183 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2185 if direction == 'bottom'
2186 for i in oldpos + 1 .. subalbums_counter
2187 subalbums_edits_bypos[i][:position] -= 1
2189 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2193 $xmldir.elements.each('dir') { |element|
2194 if (!element.attributes['deleted'])
2195 elems << [ element.attributes['path'], element.remove ]
2198 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2199 each { |e| $xmldir.add_element(e[1]) }
2200 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2201 $xmldir.elements.each('descendant::dir') { |elem|
2202 elem.delete_attribute('already-generated')
2205 sel = $albums_tv.selection.selected_rows
2207 populate_subalbums_treeview(false)
2208 $albums_tv.selection.select_path(sel[0])
2211 color_swap_and_cleanup = proc {
2212 perform_color_swap_and_cleanup = proc {
2213 color_swap(xmldir, "#{infotype}-")
2214 my_gen_real_thumbnail.call
2216 perform_color_swap_and_cleanup.call
2218 save_undo(_("color swap"),
2220 perform_color_swap_and_cleanup.call
2221 $notebook.set_page(0)
2223 perform_color_swap_and_cleanup.call
2224 $notebook.set_page(0)
2229 change_frame_offset_and_cleanup = proc {
2230 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2231 perform_change_frame_offset_and_cleanup = proc { |val|
2232 change_frame_offset(xmldir, "#{infotype}-", val)
2233 my_gen_real_thumbnail.call
2235 perform_change_frame_offset_and_cleanup.call(values[:new])
2237 save_undo(_("specify frame offset"),
2239 perform_change_frame_offset_and_cleanup.call(values[:old])
2240 $notebook.set_page(0)
2242 perform_change_frame_offset_and_cleanup.call(values[:new])
2243 $notebook.set_page(0)
2249 whitebalance_and_cleanup = proc {
2250 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2251 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2252 perform_change_whitebalance_and_cleanup = proc { |val|
2253 change_whitebalance(xmldir, "#{infotype}-", val)
2254 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2255 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2256 system("rm -f '#{thumbnail_file}'")
2258 perform_change_whitebalance_and_cleanup.call(values[:new])
2260 save_undo(_("fix white balance"),
2262 perform_change_whitebalance_and_cleanup.call(values[:old])
2263 $notebook.set_page(0)
2265 perform_change_whitebalance_and_cleanup.call(values[:new])
2266 $notebook.set_page(0)
2272 enhance_and_cleanup = proc {
2273 perform_enhance_and_cleanup = proc {
2274 enhance(xmldir, "#{infotype}-")
2275 my_gen_real_thumbnail.call
2278 perform_enhance_and_cleanup.call
2280 save_undo(_("enhance"),
2282 perform_enhance_and_cleanup.call
2283 $notebook.set_page(0)
2285 perform_enhance_and_cleanup.call
2286 $notebook.set_page(0)
2291 evtbox.signal_connect('button-press-event') { |w, event|
2292 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2294 rotate_and_cleanup.call(90)
2296 rotate_and_cleanup.call(-90)
2297 elsif $enhance.active?
2298 enhance_and_cleanup.call
2301 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2302 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2303 { :forbid_left => true, :forbid_right => true,
2304 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2305 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2306 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2307 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
2309 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2314 evtbox.signal_connect('button-press-event') { |w, event|
2315 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2319 evtbox.signal_connect('button-release-event') { |w, event|
2320 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2321 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2322 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2323 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2324 msg 3, "gesture rotate: #{angle}"
2325 rotate_and_cleanup.call(angle)
2328 $gesture_press = nil
2331 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2332 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2333 current_y_sub_albums += 1
2336 if $xmldir.child_byname_notattr('dir', 'deleted')
2338 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2339 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2340 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2341 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2342 #- this album image/caption
2343 if $xmldir.attributes['thumbnails-caption']
2344 add_subalbum.call($xmldir, 0)
2347 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2348 $xmldir.elements.each { |element|
2349 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2350 #- element (image or video) of this album
2351 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2352 msg 3, "dest_img: #{dest_img}"
2353 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2354 total[element.name] += 1
2356 if element.name == 'dir' && !element.attributes['deleted']
2357 #- sub-album image/caption
2358 add_subalbum.call(element, subalbums_counter += 1)
2359 total[element.name] += 1
2362 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2363 total['image'], total['video'], total['dir'] ]))
2364 $subalbums_vb.add($subalbums)
2365 $subalbums_vb.show_all
2367 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2368 $notebook.get_tab_label($autotable_sw).sensitive = false
2369 $notebook.set_page(0)
2370 $thumbnails_title.buffer.text = ''
2372 $notebook.get_tab_label($autotable_sw).sensitive = true
2373 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2376 if !$xmldir.child_byname_notattr('dir', 'deleted')
2377 $notebook.get_tab_label($subalbums_sw).sensitive = false
2378 $notebook.set_page(1)
2380 $notebook.get_tab_label($subalbums_sw).sensitive = true
2384 def pixbuf_or_nil(filename)
2386 return Gdk::Pixbuf.new(filename)
2392 def theme_choose(current)
2393 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2395 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2396 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2397 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2399 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2400 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2401 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2402 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2403 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2404 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2405 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2406 treeview.signal_connect('button-press-event') { |w, event|
2407 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2408 dialog.response(Gtk::Dialog::RESPONSE_OK)
2412 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2414 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2417 iter[0] = File.basename(dir)
2418 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2419 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2420 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2421 if File.basename(dir) == current
2422 treeview.selection.select_iter(iter)
2426 dialog.set_default_size(700, 400)
2427 dialog.vbox.show_all
2428 dialog.run { |response|
2429 iter = treeview.selection.selected
2431 if response == Gtk::Dialog::RESPONSE_OK && iter
2432 return model.get_value(iter, 0)
2438 def show_password_protections
2439 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2440 child_iter = $albums_iters[xmldir.attributes['path']]
2441 if xmldir.attributes['password-protect']
2442 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2443 already_protected = true
2444 elsif already_protected
2445 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2447 pix = pix.saturate_and_pixelate(1, true)
2453 xmldir.elements.each('dir') { |elem|
2454 if !elem.attributes['deleted']
2455 examine_dir_elem.call(child_iter, elem, already_protected)
2459 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2462 def populate_subalbums_treeview(select_first)
2466 $subalbums_vb.children.each { |chld|
2467 $subalbums_vb.remove(chld)
2470 source = $xmldoc.root.attributes['source']
2471 msg 3, "source: #{source}"
2473 xmldir = $xmldoc.elements['//dir']
2474 if !xmldir || xmldir.attributes['path'] != source
2475 msg 1, _("Corrupted booh file...")
2479 append_dir_elem = proc { |parent_iter, xmldir|
2480 child_iter = $albums_ts.append(parent_iter)
2481 child_iter[0] = File.basename(xmldir.attributes['path'])
2482 child_iter[1] = xmldir.attributes['path']
2483 $albums_iters[xmldir.attributes['path']] = child_iter
2484 msg 3, "puttin location: #{xmldir.attributes['path']}"
2485 xmldir.elements.each('dir') { |elem|
2486 if !elem.attributes['deleted']
2487 append_dir_elem.call(child_iter, elem)
2491 append_dir_elem.call(nil, xmldir)
2492 show_password_protections
2494 $albums_tv.expand_all
2496 $albums_tv.selection.select_iter($albums_ts.iter_first)
2500 def open_file(filename)
2504 $current_path = nil #- invalidate
2505 $modified_pixbufs = {}
2508 $subalbums_vb.children.each { |chld|
2509 $subalbums_vb.remove(chld)
2512 if !File.exists?(filename)
2513 return utf8(_("File not found."))
2517 $xmldoc = REXML::Document.new File.new(filename)
2522 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2523 if entry2type(filename).nil?
2524 return utf8(_("Not a booh file!"))
2526 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."))
2530 if !source = $xmldoc.root.attributes['source']
2531 return utf8(_("Corrupted booh file..."))
2534 if !dest = $xmldoc.root.attributes['destination']
2535 return utf8(_("Corrupted booh file..."))
2538 if !theme = $xmldoc.root.attributes['theme']
2539 return utf8(_("Corrupted booh file..."))
2542 if $xmldoc.root.attributes['version'] < '0.8.4'
2543 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2544 mark_document_as_dirty
2545 if $xmldoc.root.attributes['version'] < '0.8.4'
2546 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2547 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2548 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2549 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2550 if old_dest_dir != new_dest_dir
2551 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2553 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2554 xmldir.elements.each { |element|
2555 if %w(image video).include?(element.name) && !element.attributes['deleted']
2556 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2557 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2558 Dir[old_name + '*'].each { |file|
2559 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2560 file != new_file and sys("mv '#{file}' '#{new_file}'")
2563 if element.name == 'dir' && !element.attributes['deleted']
2564 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2565 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2566 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2570 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2574 $xmldoc.root.add_attribute('version', $VERSION)
2577 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2578 optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2579 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2581 $filename = filename
2582 select_theme(theme, limit_sizes, optimizefor32, nperrow)
2583 $default_size['thumbnails'] =~ /(.*)x(.*)/
2584 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2585 $albums_thumbnail_size =~ /(.*)x(.*)/
2586 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2588 populate_subalbums_treeview(true)
2590 $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
2594 def open_file_user(filename)
2595 result = open_file(filename)
2597 $config['last-opens'] ||= []
2598 if $config['last-opens'][-1] != utf8(filename)
2599 $config['last-opens'] << utf8(filename)
2601 $orig_filename = $filename
2602 tmp = Tempfile.new("boohtemp")
2605 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2607 $tempfiles << $filename << "#{$filename}.backup"
2609 $orig_filename = nil
2615 if !ask_save_modifications(utf8(_("Save this album?")),
2616 utf8(_("Do you want to save the changes to this album?")),
2617 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2620 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2622 Gtk::FileChooser::ACTION_OPEN,
2624 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2625 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2626 fc.set_current_folder(File.expand_path("~/.booh"))
2627 fc.transient_for = $main_window
2630 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2631 push_mousecursor_wait(fc)
2632 msg = open_file_user(fc.filename)
2648 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2653 def additional_booh_options
2656 options += "--mproc #{$config['mproc'].to_i} "
2658 options += "--comments-format '#{$config['comments-format']}'"
2663 if !ask_save_modifications(utf8(_("Save this album?")),
2664 utf8(_("Do you want to save the changes to this album?")),
2665 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2668 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2670 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2671 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2672 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2674 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2675 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2676 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2677 tbl.attach(src = Gtk::Entry.new,
2678 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2679 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2680 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2681 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2682 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2683 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2684 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2685 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2686 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2687 tbl.attach(dest = Gtk::Entry.new,
2688 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2689 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2690 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2691 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2692 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2693 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2694 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2695 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2696 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2698 tooltips = Gtk::Tooltips.new
2699 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2700 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2701 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2702 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2703 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2704 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2705 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)
2706 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2707 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2708 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2709 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2710 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)
2712 src_nb_calculated_for = ''
2714 process_src_nb = proc {
2715 if src.text != src_nb_calculated_for
2716 src_nb_calculated_for = src.text
2718 Thread.kill(src_nb_thread)
2721 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
2722 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
2724 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2725 if File.readable?(from_utf8_safe(src_nb_calculated_for))
2726 src_nb_thread = Thread.new {
2727 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2728 total = { 'image' => 0, 'video' => 0, nil => 0 }
2729 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2730 if File.basename(dir) =~ /^\./
2734 Dir.entries(dir.chomp).each { |file|
2735 total[entry2type(file)] += 1
2737 rescue Errno::EACCES, Errno::ENOENT
2741 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2745 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2748 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2754 timeout_src_nb = Gtk.timeout_add(100) {
2758 src_browse.signal_connect('clicked') {
2759 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2761 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2763 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2764 fc.transient_for = $main_window
2765 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2766 src.text = utf8(fc.filename)
2768 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2773 dest_browse.signal_connect('clicked') {
2774 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2776 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2778 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2779 fc.transient_for = $main_window
2780 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2781 dest.text = utf8(fc.filename)
2786 conf_browse.signal_connect('clicked') {
2787 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2789 Gtk::FileChooser::ACTION_SAVE,
2791 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2792 fc.transient_for = $main_window
2793 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2794 fc.set_current_folder(File.expand_path("~/.booh"))
2795 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2796 conf.text = utf8(fc.filename)
2803 recreate_theme_config = proc {
2804 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2806 select_theme(theme_button.label, 'all', optimize432.active?, nil)
2807 $images_size.each { |s|
2808 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2812 tooltips.set_tip(cb, utf8(s['description']), nil)
2813 theme_sizes << { :widget => cb, :value => s['name'] }
2815 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2816 tooltips = Gtk::Tooltips.new
2817 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2818 theme_sizes << { :widget => cb, :value => 'original' }
2821 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2824 $allowed_N_values.each { |n|
2826 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2828 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2830 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2834 nperrows << { :widget => rb, :value => n }
2836 nperrowradios.show_all
2838 recreate_theme_config.call
2840 theme_button.signal_connect('clicked') {
2841 if newtheme = theme_choose(theme_button.label)
2842 theme_button.label = newtheme
2843 recreate_theme_config.call
2847 dialog.vbox.add(frame1)
2848 dialog.vbox.add(frame2)
2849 dialog.window_position = Gtk::Window::POS_MOUSE
2855 dialog.run { |response|
2856 if response == Gtk::Dialog::RESPONSE_OK
2857 srcdir = from_utf8_safe(src.text)
2858 destdir = from_utf8_safe(dest.text)
2859 confpath = from_utf8_safe(conf.text)
2860 if src.text != '' && srcdir == ''
2861 show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
2863 elsif !File.directory?(srcdir)
2864 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2866 elsif dest.text != '' && destdir == ''
2867 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
2869 elsif destdir != make_dest_filename(destdir)
2870 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2872 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
2873 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2875 elsif File.exists?(destdir) && !File.directory?(destdir)
2876 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2878 elsif conf.text == ''
2879 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2881 elsif conf.text != '' && confpath == ''
2882 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
2884 elsif File.directory?(confpath)
2885 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
2887 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2888 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2890 system("mkdir '#{destdir}'")
2891 if !File.directory?(destdir)
2892 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2904 srcdir = from_utf8(src.text)
2905 destdir = from_utf8(dest.text)
2906 configskel = File.expand_path(from_utf8(conf.text))
2907 theme = theme_button.label
2908 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2909 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2910 opt432 = optimize432.active?
2911 madewith = madewithentry.text
2914 Thread.kill(src_nb_thread)
2915 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2918 Gtk.timeout_remove(timeout_src_nb)
2921 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2922 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2923 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' #{additional_booh_options}",
2924 utf8(_("Please wait while scanning source directory...")),
2926 { :closure_after => proc { open_file_user(configskel) } })
2931 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2933 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2934 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2935 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2937 source = $xmldoc.root.attributes['source']
2938 dest = $xmldoc.root.attributes['destination']
2939 theme = $xmldoc.root.attributes['theme']
2940 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2941 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2942 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2944 limit_sizes = limit_sizes.split(/,/)
2946 madewith = $xmldoc.root.attributes['made-with']
2948 tooltips = Gtk::Tooltips.new
2949 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2950 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2951 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2952 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2953 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2954 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2955 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2956 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
2957 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2958 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2959 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2960 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
2961 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2963 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2964 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2965 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2966 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2967 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2968 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2969 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)
2970 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2971 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2972 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2973 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2975 madewithentry.text = madewith
2977 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)
2981 recreate_theme_config = proc {
2982 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2984 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
2986 $images_size.each { |s|
2987 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2989 if limit_sizes.include?(s['name'])
2997 tooltips.set_tip(cb, utf8(s['description']), nil)
2998 theme_sizes << { :widget => cb, :value => s['name'] }
3000 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3001 tooltips = Gtk::Tooltips.new
3002 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3003 if limit_sizes && limit_sizes.include?('original')
3006 theme_sizes << { :widget => cb, :value => 'original' }
3009 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3012 $allowed_N_values.each { |n|
3014 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3016 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3018 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3019 nperrowradios.add(Gtk::Label.new(' '))
3020 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3023 nperrows << { :widget => rb, :value => n.to_s }
3025 nperrowradios.show_all
3027 recreate_theme_config.call
3029 theme_button.signal_connect('clicked') {
3030 if newtheme = theme_choose(theme_button.label)
3033 theme_button.label = newtheme
3034 recreate_theme_config.call
3038 dialog.vbox.add(frame1)
3039 dialog.vbox.add(frame2)
3040 dialog.window_position = Gtk::Window::POS_MOUSE
3046 dialog.run { |response|
3047 if response == Gtk::Dialog::RESPONSE_OK
3048 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3049 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3058 save_theme = theme_button.label
3059 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3060 save_opt432 = optimize432.active?
3061 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3062 save_madewith = madewithentry.text
3065 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith)
3066 mark_document_as_dirty
3068 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3069 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3070 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' #{additional_booh_options}",
3071 utf8(_("Please wait while scanning source directory...")),
3073 { :closure_after => proc {
3074 open_file($filename)
3083 sel = $albums_tv.selection.selected_rows
3085 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3086 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3087 utf8(_("Please wait while scanning source directory...")),
3089 { :closure_after => proc {
3090 open_file($filename)
3091 $albums_tv.selection.select_path(sel[0])
3099 sel = $albums_tv.selection.selected_rows
3101 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3102 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3103 utf8(_("Please wait while scanning source directory...")),
3105 { :closure_after => proc {
3106 open_file($filename)
3107 $albums_tv.selection.select_path(sel[0])
3115 theme = $xmldoc.root.attributes['theme']
3116 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3118 limit_sizes = "--sizes #{limit_sizes}"
3120 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3121 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3122 utf8(_("Please wait while scanning source directory...")),
3124 { :closure_after => proc {
3125 open_file($filename)
3131 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3133 Gtk::FileChooser::ACTION_SAVE,
3135 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3136 fc.transient_for = $main_window
3137 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3138 fc.set_current_folder(File.expand_path("~/.booh"))
3139 fc.filename = $orig_filename
3140 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3141 $orig_filename = fc.filename
3142 if ! save_current_file_user
3146 $config['last-opens'] ||= []
3147 $config['last-opens'] << $orig_filename
3153 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3155 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3156 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3157 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3159 dialog.vbox.add(notebook = Gtk::Notebook.new)
3160 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3161 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3162 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3163 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)),
3164 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3165 tooltips = Gtk::Tooltips.new
3166 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3167 for example: /usr/bin/mplayer %f")), nil)
3168 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3169 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3170 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3171 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3172 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3173 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3174 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3175 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3176 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)),
3177 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3178 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)
3179 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3180 0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3181 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)
3182 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3183 0, 2, 5, 6, Gtk::FILL, Gtk::SHRINK, 2, 2)
3184 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)
3186 smp_check.signal_connect('toggled') {
3187 if smp_check.active?
3188 smp_hbox.sensitive = true
3190 smp_hbox.sensitive = false
3194 smp_check.active = true
3195 smp_spin.value = $config['mproc'].to_i
3197 nogestures_check.active = $config['nogestures']
3198 deleteondisk_check.active = $config['deleteondisk']
3200 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3201 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3202 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3203 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3204 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3205 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3206 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3207 tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3208 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3209 tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3210 2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3211 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)
3212 commentsformat_help.signal_connect('clicked') {
3213 show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3214 hence you should look at ImageMagick/identify documentation for the most
3215 accurate and up-to-date documentation. Last time I checked, documentation
3218 Print information about the image in a format of your choosing. You can
3219 include the image filename, type, width, height, Exif data, or other image
3220 attributes by embedding special format characters:
3223 %P page width and height
3227 %e filename extension
3232 %k number of unique colors
3239 %r image class and colorspace
3242 %u unique temporary filename
3255 displays MIFF:bird.miff 512x480 for an image titled bird.miff and whose
3256 width is 512 and height is 480.
3258 If the first character of string is @, the format is read from a file titled
3259 by the remaining characters in the string.
3261 You can also use the following special formatting syntax to print Exif
3262 information contained in the file:
3266 Where tag can be one of the following:
3268 * (print all Exif tags, in keyword=data format)
3269 ! (print all Exif tags, in tag_number data format)
3270 #hhhh (print data for Exif tag #hhhh)
3275 PhotometricInterpretation
3295 PrimaryChromaticities
3298 JPEGInterchangeFormat
3299 JPEGInterchangeFormatLength
3321 ComponentsConfiguration
3322 CompressedBitsPerPixel
3342 InteroperabilityOffset
3344 SpatialFrequencyResponse
3345 FocalPlaneXResolution
3346 FocalPlaneYResolution
3347 FocalPlaneResolutionUnit
3352 SceneType")), { :scrolled => true })
3355 dialog.vbox.show_all
3356 dialog.run { |response|
3357 if response == Gtk::Dialog::RESPONSE_OK
3358 $config['video-viewer'] = video_viewer_entry.text
3359 $config['browser'] = browser_entry.text
3360 if smp_check.active?
3361 $config['mproc'] = smp_spin.value.to_i
3363 $config.delete('mproc')
3365 $config['nogestures'] = nogestures_check.active?
3366 $config['deleteondisk'] = deleteondisk_check.active?
3368 $config['convert-enhance'] = enhance_entry.text
3369 $config['comments-format'] = commentsformat_entry.text.gsub(/'/, '')
3376 if $undo_tb.sensitive?
3377 $redo_tb.sensitive = $redo_mb.sensitive = true
3378 if not more_undoes = UndoHandler.undo($statusbar)
3379 $undo_tb.sensitive = $undo_mb.sensitive = false
3385 if $redo_tb.sensitive?
3386 $undo_tb.sensitive = $undo_mb.sensitive = true
3387 if not more_redoes = UndoHandler.redo($statusbar)
3388 $redo_tb.sensitive = $redo_mb.sensitive = false
3393 def show_one_click_explanation(intro)
3394 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
3396 %s When such a tool is activated
3397 (<span foreground='darkblue'>Rotate clockwise</span>, <span foreground='darkblue'>Rotate counter-clockwise</span>, <span foreground='darkblue'>Enhance</span> or <span foreground='darkblue'>Delete</span>), clicking
3398 on a thumbnail will immediately apply the desired action.
3400 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
3401 ") % intro), { :pos_centered => true })
3406 GNU GENERAL PUBLIC LICENSE
3407 Version 2, June 1991
3409 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
3410 675 Mass Ave, Cambridge, MA 02139, USA
3411 Everyone is permitted to copy and distribute verbatim copies
3412 of this license document, but changing it is not allowed.
3416 The licenses for most software are designed to take away your
3417 freedom to share and change it. By contrast, the GNU General Public
3418 License is intended to guarantee your freedom to share and change free
3419 software--to make sure the software is free for all its users. This
3420 General Public License applies to most of the Free Software
3421 Foundation's software and to any other program whose authors commit to
3422 using it. (Some other Free Software Foundation software is covered by
3423 the GNU Library General Public License instead.) You can apply it to
3426 When we speak of free software, we are referring to freedom, not
3427 price. Our General Public Licenses are designed to make sure that you
3428 have the freedom to distribute copies of free software (and charge for
3429 this service if you wish), that you receive source code or can get it
3430 if you want it, that you can change the software or use pieces of it
3431 in new free programs; and that you know you can do these things.
3433 To protect your rights, we need to make restrictions that forbid
3434 anyone to deny you these rights or to ask you to surrender the rights.
3435 These restrictions translate to certain responsibilities for you if you
3436 distribute copies of the software, or if you modify it.
3438 For example, if you distribute copies of such a program, whether
3439 gratis or for a fee, you must give the recipients all the rights that
3440 you have. You must make sure that they, too, receive or can get the
3441 source code. And you must show them these terms so they know their
3444 We protect your rights with two steps: (1) copyright the software, and
3445 (2) offer you this license which gives you legal permission to copy,
3446 distribute and/or modify the software.
3448 Also, for each author's protection and ours, we want to make certain
3449 that everyone understands that there is no warranty for this free
3450 software. If the software is modified by someone else and passed on, we
3451 want its recipients to know that what they have is not the original, so
3452 that any problems introduced by others will not reflect on the original
3453 authors' reputations.
3455 Finally, any free program is threatened constantly by software
3456 patents. We wish to avoid the danger that redistributors of a free
3457 program will individually obtain patent licenses, in effect making the
3458 program proprietary. To prevent this, we have made it clear that any
3459 patent must be licensed for everyone's free use or not licensed at all.
3461 The precise terms and conditions for copying, distribution and
3462 modification follow.
3465 GNU GENERAL PUBLIC LICENSE
3466 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
3468 0. This License applies to any program or other work which contains
3469 a notice placed by the copyright holder saying it may be distributed
3470 under the terms of this General Public License. The "Program", below,
3471 refers to any such program or work, and a "work based on the Program"
3472 means either the Program or any derivative work under copyright law:
3473 that is to say, a work containing the Program or a portion of it,
3474 either verbatim or with modifications and/or translated into another
3475 language. (Hereinafter, translation is included without limitation in
3476 the term "modification".) Each licensee is addressed as "you".
3478 Activities other than copying, distribution and modification are not
3479 covered by this License; they are outside its scope. The act of
3480 running the Program is not restricted, and the output from the Program
3481 is covered only if its contents constitute a work based on the
3482 Program (independent of having been made by running the Program).
3483 Whether that is true depends on what the Program does.
3485 1. You may copy and distribute verbatim copies of the Program's
3486 source code as you receive it, in any medium, provided that you
3487 conspicuously and appropriately publish on each copy an appropriate
3488 copyright notice and disclaimer of warranty; keep intact all the
3489 notices that refer to this License and to the absence of any warranty;
3490 and give any other recipients of the Program a copy of this License
3491 along with the Program.
3493 You may charge a fee for the physical act of transferring a copy, and
3494 you may at your option offer warranty protection in exchange for a fee.
3496 2. You may modify your copy or copies of the Program or any portion
3497 of it, thus forming a work based on the Program, and copy and
3498 distribute such modifications or work under the terms of Section 1
3499 above, provided that you also meet all of these conditions:
3501 a) You must cause the modified files to carry prominent notices
3502 stating that you changed the files and the date of any change.
3504 b) You must cause any work that you distribute or publish, that in
3505 whole or in part contains or is derived from the Program or any
3506 part thereof, to be licensed as a whole at no charge to all third
3507 parties under the terms of this License.
3509 c) If the modified program normally reads commands interactively
3510 when run, you must cause it, when started running for such
3511 interactive use in the most ordinary way, to print or display an
3512 announcement including an appropriate copyright notice and a
3513 notice that there is no warranty (or else, saying that you provide
3514 a warranty) and that users may redistribute the program under
3515 these conditions, and telling the user how to view a copy of this
3516 License. (Exception: if the Program itself is interactive but
3517 does not normally print such an announcement, your work based on
3518 the Program is not required to print an announcement.)
3521 These requirements apply to the modified work as a whole. If
3522 identifiable sections of that work are not derived from the Program,
3523 and can be reasonably considered independent and separate works in
3524 themselves, then this License, and its terms, do not apply to those
3525 sections when you distribute them as separate works. But when you
3526 distribute the same sections as part of a whole which is a work based
3527 on the Program, the distribution of the whole must be on the terms of
3528 this License, whose permissions for other licensees extend to the
3529 entire whole, and thus to each and every part regardless of who wrote it.
3531 Thus, it is not the intent of this section to claim rights or contest
3532 your rights to work written entirely by you; rather, the intent is to
3533 exercise the right to control the distribution of derivative or
3534 collective works based on the Program.
3536 In addition, mere aggregation of another work not based on the Program
3537 with the Program (or with a work based on the Program) on a volume of
3538 a storage or distribution medium does not bring the other work under
3539 the scope of this License.
3541 3. You may copy and distribute the Program (or a work based on it,
3542 under Section 2) in object code or executable form under the terms of
3543 Sections 1 and 2 above provided that you also do one of the following:
3545 a) Accompany it with the complete corresponding machine-readable
3546 source code, which must be distributed under the terms of Sections
3547 1 and 2 above on a medium customarily used for software interchange; or,
3549 b) Accompany it with a written offer, valid for at least three
3550 years, to give any third party, for a charge no more than your
3551 cost of physically performing source distribution, a complete
3552 machine-readable copy of the corresponding source code, to be
3553 distributed under the terms of Sections 1 and 2 above on a medium
3554 customarily used for software interchange; or,
3556 c) Accompany it with the information you received as to the offer
3557 to distribute corresponding source code. (This alternative is
3558 allowed only for noncommercial distribution and only if you
3559 received the program in object code or executable form with such
3560 an offer, in accord with Subsection b above.)
3562 The source code for a work means the preferred form of the work for
3563 making modifications to it. For an executable work, complete source
3564 code means all the source code for all modules it contains, plus any
3565 associated interface definition files, plus the scripts used to
3566 control compilation and installation of the executable. However, as a
3567 special exception, the source code distributed need not include
3568 anything that is normally distributed (in either source or binary
3569 form) with the major components (compiler, kernel, and so on) of the
3570 operating system on which the executable runs, unless that component
3571 itself accompanies the executable.
3573 If distribution of executable or object code is made by offering
3574 access to copy from a designated place, then offering equivalent
3575 access to copy the source code from the same place counts as
3576 distribution of the source code, even though third parties are not
3577 compelled to copy the source along with the object code.
3580 4. You may not copy, modify, sublicense, or distribute the Program
3581 except as expressly provided under this License. Any attempt
3582 otherwise to copy, modify, sublicense or distribute the Program is
3583 void, and will automatically terminate your rights under this License.
3584 However, parties who have received copies, or rights, from you under
3585 this License will not have their licenses terminated so long as such
3586 parties remain in full compliance.
3588 5. You are not required to accept this License, since you have not
3589 signed it. However, nothing else grants you permission to modify or
3590 distribute the Program or its derivative works. These actions are
3591 prohibited by law if you do not accept this License. Therefore, by
3592 modifying or distributing the Program (or any work based on the
3593 Program), you indicate your acceptance of this License to do so, and
3594 all its terms and conditions for copying, distributing or modifying
3595 the Program or works based on it.
3597 6. Each time you redistribute the Program (or any work based on the
3598 Program), the recipient automatically receives a license from the
3599 original licensor to copy, distribute or modify the Program subject to
3600 these terms and conditions. You may not impose any further
3601 restrictions on the recipients' exercise of the rights granted herein.
3602 You are not responsible for enforcing compliance by third parties to
3605 7. If, as a consequence of a court judgment or allegation of patent
3606 infringement or for any other reason (not limited to patent issues),
3607 conditions are imposed on you (whether by court order, agreement or
3608 otherwise) that contradict the conditions of this License, they do not
3609 excuse you from the conditions of this License. If you cannot
3610 distribute so as to satisfy simultaneously your obligations under this
3611 License and any other pertinent obligations, then as a consequence you
3612 may not distribute the Program at all. For example, if a patent
3613 license would not permit royalty-free redistribution of the Program by
3614 all those who receive copies directly or indirectly through you, then
3615 the only way you could satisfy both it and this License would be to
3616 refrain entirely from distribution of the Program.
3618 If any portion of this section is held invalid or unenforceable under
3619 any particular circumstance, the balance of the section is intended to
3620 apply and the section as a whole is intended to apply in other
3623 It is not the purpose of this section to induce you to infringe any
3624 patents or other property right claims or to contest validity of any
3625 such claims; this section has the sole purpose of protecting the
3626 integrity of the free software distribution system, which is
3627 implemented by public license practices. Many people have made
3628 generous contributions to the wide range of software distributed
3629 through that system in reliance on consistent application of that
3630 system; it is up to the author/donor to decide if he or she is willing
3631 to distribute software through any other system and a licensee cannot
3634 This section is intended to make thoroughly clear what is believed to
3635 be a consequence of the rest of this License.
3638 8. If the distribution and/or use of the Program is restricted in
3639 certain countries either by patents or by copyrighted interfaces, the
3640 original copyright holder who places the Program under this License
3641 may add an explicit geographical distribution limitation excluding
3642 those countries, so that distribution is permitted only in or among
3643 countries not thus excluded. In such case, this License incorporates
3644 the limitation as if written in the body of this License.
3646 9. The Free Software Foundation may publish revised and/or new versions
3647 of the General Public License from time to time. Such new versions will
3648 be similar in spirit to the present version, but may differ in detail to
3649 address new problems or concerns.
3651 Each version is given a distinguishing version number. If the Program
3652 specifies a version number of this License which applies to it and "any
3653 later version", you have the option of following the terms and conditions
3654 either of that version or of any later version published by the Free
3655 Software Foundation. If the Program does not specify a version number of
3656 this License, you may choose any version ever published by the Free Software
3659 10. If you wish to incorporate parts of the Program into other free
3660 programs whose distribution conditions are different, write to the author
3661 to ask for permission. For software which is copyrighted by the Free
3662 Software Foundation, write to the Free Software Foundation; we sometimes
3663 make exceptions for this. Our decision will be guided by the two goals
3664 of preserving the free status of all derivatives of our free software and
3665 of promoting the sharing and reuse of software generally.
3669 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
3670 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
3671 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
3672 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
3673 OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
3674 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
3675 TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
3676 PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
3677 REPAIR OR CORRECTION.
3679 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
3680 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
3681 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
3682 INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
3683 OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
3684 TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
3685 YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
3686 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
3687 POSSIBILITY OF SUCH DAMAGES.
3691 def create_menu_and_toolbar
3694 mb = Gtk::MenuBar.new
3696 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
3697 filesubmenu = Gtk::Menu.new
3698 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
3699 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
3700 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3701 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
3702 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
3703 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3704 tooltips = Gtk::Tooltips.new
3705 filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
3706 $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3707 tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
3708 filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
3709 $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3710 tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
3711 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
3712 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3713 tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums (anywhere)")), nil)
3714 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3715 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
3716 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3717 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
3718 filesubmenu.append($view_wa = Gtk::ImageMenuItem.new(utf8(_("View web-album with browser"))).set_sensitive(false))
3719 $view_wa.image = Gtk::Image.new("#{$FPATH}/images/stock-view-webalbum-16.png")
3720 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3721 filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
3722 tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
3723 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3724 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
3725 filemenu.set_submenu(filesubmenu)
3728 new.signal_connect('activate') { new_album }
3729 open.signal_connect('activate') { open_file_popup }
3730 $save.signal_connect('activate') { save_current_file_user }
3731 $save_as.signal_connect('activate') { save_as_do }
3732 $merge_current.signal_connect('activate') { merge_current }
3733 $merge_newsubs.signal_connect('activate') { merge_newsubs }
3734 $merge.signal_connect('activate') { merge }
3735 $generate.signal_connect('activate') {
3737 call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
3738 utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
3740 { :successmsg => utf8(_("Your web-album is now ready in directory '%s'.
3741 Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
3742 :successmsg_linkurl => $xmldoc.root.attributes['destination'],
3743 :closure_after => proc {
3744 $xmldoc.elements.each('//dir') { |elem|
3745 $modified ||= elem.attributes['already-generated'].nil?
3746 elem.add_attribute('already-generated', 'true')
3748 UndoHandler.cleanup #- prevent save_changes to mark current dir as not already generated
3749 $undo_tb.sensitive = $undo_mb.sensitive = false
3750 $redo_tb.sensitive = $redo_mb.sensitive = false
3752 $generated_outofline = true
3755 $view_wa.signal_connect('activate') {
3756 indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
3757 if File.exists?(indexhtml)
3760 show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
3763 $properties.signal_connect('activate') { properties }
3765 quit.signal_connect('activate') { try_quit }
3767 editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
3768 editsubmenu = Gtk::Menu.new
3769 editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
3770 editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
3771 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3772 editsubmenu.append($sort_by_exif_date = Gtk::ImageMenuItem.new(utf8(_("Sort by EXIF date"))).set_sensitive(false))
3773 $sort_by_exif_date.image = Gtk::Image.new("#{$FPATH}/images/sort_by_exif_date.png")
3774 editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
3775 $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
3776 tooltips.set_tip($remove_all_captions, utf8(_("Mainly useful when you don't want to type any caption, that will remove default captions made of filenames")), nil)
3777 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3778 editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
3779 editmenu.set_submenu(editsubmenu)
3782 $remove_all_captions.signal_connect('activate') { remove_all_captions }
3783 $sort_by_exif_date.signal_connect('activate') { sort_by_exif_date }
3785 prefs.signal_connect('activate') { preferences }
3787 helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
3788 helpsubmenu = Gtk::Menu.new
3789 helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
3790 one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
3791 helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
3792 speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
3793 helpsubmenu.append(tutos = Gtk::ImageMenuItem.new(utf8(_("Online tutorials (opens a web-browser)"))))
3794 tutos.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3795 helpsubmenu.append(Gtk::SeparatorMenuItem.new)
3796 helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
3797 helpmenu.set_submenu(helpsubmenu)
3800 one_click.signal_connect('activate') {
3801 show_one_click_explanation(_("One-Click tools are available in the toolbar."))
3804 speed.signal_connect('activate') {
3805 show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
3807 <span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
3808 <span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
3809 <span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
3810 <span foreground='darkblue'>Control-Enter</span>: for an image, open larger view; for a video, launch player
3811 <span foreground='darkblue'>Control-Delete</span>: delete image
3812 <span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
3813 <span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
3814 <span foreground='darkblue'>Control-z</span>: undo
3815 <span foreground='darkblue'>Control-r</span>: redo
3817 <span size='large' weight='bold'>Mouse gestures:</span>
3819 Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
3820 for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
3822 <span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
3823 <span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
3824 <span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
3825 <span foreground='darkblue'>Left click, hold left button, right click</span>: undo
3826 <span foreground='darkblue'>Right click, hold right button, left click</span>: redo
3827 ")), { :pos_centered => true, :not_transient => true })
3830 tutos.signal_connect('activate') {
3831 open_url('http://zarb.org/~gc/html/booh/tutorial.html')
3834 about.signal_connect('activate') {
3835 Gtk::AboutDialog.set_url_hook { |dialog, url| open_url(url) }
3836 Gtk::AboutDialog.show($main_window, { :name => 'booh',
3837 :version => $VERSION,
3838 :copyright => 'Copyright (c) 2005 Guillaume Cottenceau',
3839 :license => get_license,
3840 :website => 'http://zarb.org/~gc/html/booh.html',
3841 :authors => [ 'Guillaume Cottenceau' ],
3842 :artists => [ 'Ayo73' ],
3843 :comments => utf8(_("''The Web-Album of choice for discriminating Linux users''")),
3844 :translator_credits => utf8(_('Japanese: Masao Mutoh
3845 German: Roland Eckert
3846 French: Guillaume Cottenceau')),
3847 :logo => Gdk::Pixbuf.new("#{$FPATH}/images/logo.png") })
3852 tb = Gtk::Toolbar.new
3854 tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
3855 open.label = utf8(_("Open")) #- to avoid missing gtk2 l10n catalogs
3856 open.menu = Gtk::Menu.new
3857 open.signal_connect('clicked') { open_file_popup }
3858 open.signal_connect('show-menu') {
3859 lastopens = Gtk::Menu.new
3861 if $config['last-opens']
3862 $config['last-opens'].reverse.each { |e|
3863 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
3864 item.signal_connect('activate') {
3865 if ask_save_modifications(utf8(_("Save this album?")),
3866 utf8(_("Do you want to save the changes to this album?")),
3867 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3868 push_mousecursor_wait
3869 msg = open_file_user(from_utf8(e))
3872 show_popup($main_window, msg)
3880 open.menu = lastopens
3883 tb.insert(-1, Gtk::SeparatorToolItem.new)
3885 tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
3886 $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
3887 $r90.label = utf8(_("Rotate"))
3888 tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
3889 $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
3890 $r270.label = utf8(_("Rotate"))
3891 tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
3892 $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
3893 $enhance.label = utf8(_("Enhance"))
3894 tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
3895 $delete.label = utf8(_("Delete")) #- to avoid missing gtk2 l10n catalogs
3896 tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
3897 nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
3898 nothing.label = utf8(_("None"))
3900 tb.insert(-1, Gtk::SeparatorToolItem.new)
3902 tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
3903 tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
3906 $undo_tb.signal_connect('clicked') { perform_undo }
3907 $undo_mb.signal_connect('activate') { perform_undo }
3908 $redo_tb.signal_connect('clicked') { perform_redo }
3909 $redo_mb.signal_connect('activate') { perform_redo }
3911 one_click_explain_try = proc {
3912 if !$config['one-click-explained']
3913 show_one_click_explanation(_("You have just clicked on a One-Click tool."))
3914 $config['one-click-explained'] = true
3918 $r90.signal_connect('toggled') {
3920 set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
3921 one_click_explain_try.call
3922 $r270.active = false
3923 $enhance.active = false
3924 $delete.active = false
3925 nothing.sensitive = true
3927 if !$r270.active? && !$enhance.active? && !$delete.active?
3928 set_mousecursor_normal
3929 nothing.sensitive = false
3931 nothing.sensitive = true
3935 $r270.signal_connect('toggled') {
3937 set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
3938 one_click_explain_try.call
3940 $enhance.active = false
3941 $delete.active = false
3942 nothing.sensitive = true
3944 if !$r90.active? && !$enhance.active? && !$delete.active?
3945 set_mousecursor_normal
3946 nothing.sensitive = false
3948 nothing.sensitive = true
3952 $enhance.signal_connect('toggled') {
3954 set_mousecursor(Gdk::Cursor::SPRAYCAN)
3955 one_click_explain_try.call
3957 $r270.active = false
3958 $delete.active = false
3959 nothing.sensitive = true
3961 if !$r90.active? && !$r270.active? && !$delete.active?
3962 set_mousecursor_normal
3963 nothing.sensitive = false
3965 nothing.sensitive = true
3969 $delete.signal_connect('toggled') {
3971 set_mousecursor(Gdk::Cursor::PIRATE)
3972 one_click_explain_try.call
3974 $r270.active = false
3975 $enhance.active = false
3976 nothing.sensitive = true
3978 if !$r90.active? && !$r270.active? && !$enhance.active?
3979 set_mousecursor_normal
3980 nothing.sensitive = false
3982 nothing.sensitive = true
3986 nothing.signal_connect('clicked') {
3987 $r90.active = $r270.active = $enhance.active = $delete.active = false
3988 set_mousecursor_normal
3994 def gtk_thread_protect(&proc)
3995 if Thread.current == Thread.main