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 File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2722 if File.readable?(from_utf8(src_nb_calculated_for))
2723 src_nb_thread = Thread.new {
2724 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2725 total = { 'image' => 0, 'video' => 0, nil => 0 }
2726 `find '#{from_utf8(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2727 if File.basename(dir) =~ /^\./
2731 Dir.entries(dir.chomp).each { |file|
2732 total[entry2type(file)] += 1
2734 rescue Errno::EACCES, Errno::ENOENT
2738 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2742 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2745 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2750 timeout_src_nb = Gtk.timeout_add(100) {
2754 src_browse.signal_connect('clicked') {
2755 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2757 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2759 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2760 fc.transient_for = $main_window
2761 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2762 src.text = utf8(fc.filename)
2764 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2769 dest_browse.signal_connect('clicked') {
2770 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2772 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2774 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2775 fc.transient_for = $main_window
2776 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2777 dest.text = utf8(fc.filename)
2782 conf_browse.signal_connect('clicked') {
2783 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2785 Gtk::FileChooser::ACTION_SAVE,
2787 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2788 fc.transient_for = $main_window
2789 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2790 fc.set_current_folder(File.expand_path("~/.booh"))
2791 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2792 conf.text = utf8(fc.filename)
2799 recreate_theme_config = proc {
2800 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2802 select_theme(theme_button.label, 'all', optimize432.active?, nil)
2803 $images_size.each { |s|
2804 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2808 tooltips.set_tip(cb, utf8(s['description']), nil)
2809 theme_sizes << { :widget => cb, :value => s['name'] }
2811 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2812 tooltips = Gtk::Tooltips.new
2813 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2814 theme_sizes << { :widget => cb, :value => 'original' }
2817 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2820 $allowed_N_values.each { |n|
2822 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2824 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2826 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2830 nperrows << { :widget => rb, :value => n }
2832 nperrowradios.show_all
2834 recreate_theme_config.call
2836 theme_button.signal_connect('clicked') {
2837 if newtheme = theme_choose(theme_button.label)
2838 theme_button.label = newtheme
2839 recreate_theme_config.call
2843 dialog.vbox.add(frame1)
2844 dialog.vbox.add(frame2)
2845 dialog.window_position = Gtk::Window::POS_MOUSE
2851 dialog.run { |response|
2852 if response == Gtk::Dialog::RESPONSE_OK
2853 srcdir = from_utf8(src.text)
2854 destdir = from_utf8(dest.text)
2855 if !File.directory?(srcdir)
2856 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2858 elsif conf.text == ''
2859 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2861 elsif File.directory?(from_utf8(conf.text))
2862 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
2864 elsif destdir != make_dest_filename(destdir)
2865 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2867 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
2868 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2870 elsif File.exists?(destdir) && !File.directory?(destdir)
2871 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2873 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2874 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2876 system("mkdir '#{destdir}'")
2877 if !File.directory?(destdir)
2878 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2889 srcdir = from_utf8(src.text)
2890 destdir = from_utf8(dest.text)
2891 configskel = File.expand_path(from_utf8(conf.text))
2892 theme = theme_button.label
2893 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2894 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2895 opt432 = optimize432.active?
2896 madewith = madewithentry.text
2898 Thread.kill(src_nb_thread)
2899 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2902 Gtk.timeout_remove(timeout_src_nb)
2905 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2906 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2907 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' #{additional_booh_options}",
2908 utf8(_("Please wait while scanning source directory...")),
2910 { :closure_after => proc { open_file_user(configskel) } })
2915 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2917 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2918 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2919 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2921 source = $xmldoc.root.attributes['source']
2922 dest = $xmldoc.root.attributes['destination']
2923 theme = $xmldoc.root.attributes['theme']
2924 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2925 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2926 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2928 limit_sizes = limit_sizes.split(/,/)
2930 madewith = $xmldoc.root.attributes['made-with']
2932 tooltips = Gtk::Tooltips.new
2933 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2934 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2935 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2936 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2937 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2938 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2939 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2940 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
2941 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2942 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2943 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2944 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
2945 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2947 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2948 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2949 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2950 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2951 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2952 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2953 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)
2954 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2955 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2956 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2957 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2959 madewithentry.text = madewith
2961 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)
2965 recreate_theme_config = proc {
2966 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2968 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
2970 $images_size.each { |s|
2971 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2973 if limit_sizes.include?(s['name'])
2981 tooltips.set_tip(cb, utf8(s['description']), nil)
2982 theme_sizes << { :widget => cb, :value => s['name'] }
2984 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2985 tooltips = Gtk::Tooltips.new
2986 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2987 if limit_sizes && limit_sizes.include?('original')
2990 theme_sizes << { :widget => cb, :value => 'original' }
2993 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2996 $allowed_N_values.each { |n|
2998 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3000 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3002 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3003 nperrowradios.add(Gtk::Label.new(' '))
3004 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3007 nperrows << { :widget => rb, :value => n.to_s }
3009 nperrowradios.show_all
3011 recreate_theme_config.call
3013 theme_button.signal_connect('clicked') {
3014 if newtheme = theme_choose(theme_button.label)
3017 theme_button.label = newtheme
3018 recreate_theme_config.call
3022 dialog.vbox.add(frame1)
3023 dialog.vbox.add(frame2)
3024 dialog.window_position = Gtk::Window::POS_MOUSE
3030 dialog.run { |response|
3031 if response == Gtk::Dialog::RESPONSE_OK
3032 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3033 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3042 save_theme = theme_button.label
3043 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3044 save_opt432 = optimize432.active?
3045 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3046 save_madewith = madewithentry.text
3049 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith)
3050 mark_document_as_dirty
3052 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3053 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3054 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' #{additional_booh_options}",
3055 utf8(_("Please wait while scanning source directory...")),
3057 { :closure_after => proc {
3058 open_file($filename)
3067 sel = $albums_tv.selection.selected_rows
3069 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3070 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3071 utf8(_("Please wait while scanning source directory...")),
3073 { :closure_after => proc {
3074 open_file($filename)
3075 $albums_tv.selection.select_path(sel[0])
3083 sel = $albums_tv.selection.selected_rows
3085 call_backend("booh-backend --merge-config-subdirs '#{$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 theme = $xmldoc.root.attributes['theme']
3100 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3102 limit_sizes = "--sizes #{limit_sizes}"
3104 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3105 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3106 utf8(_("Please wait while scanning source directory...")),
3108 { :closure_after => proc {
3109 open_file($filename)
3115 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3117 Gtk::FileChooser::ACTION_SAVE,
3119 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3120 fc.transient_for = $main_window
3121 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3122 fc.set_current_folder(File.expand_path("~/.booh"))
3123 fc.filename = $orig_filename
3124 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3125 $orig_filename = fc.filename
3126 if ! save_current_file_user
3130 $config['last-opens'] ||= []
3131 $config['last-opens'] << $orig_filename
3137 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3139 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3140 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3141 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3143 dialog.vbox.add(notebook = Gtk::Notebook.new)
3144 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3145 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3146 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3147 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)),
3148 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3149 tooltips = Gtk::Tooltips.new
3150 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3151 for example: /usr/bin/mplayer %f")), nil)
3152 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3153 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3154 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3155 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3156 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3157 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3158 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3159 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3160 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)),
3161 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3162 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)
3163 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3164 0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3165 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)
3166 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3167 0, 2, 5, 6, Gtk::FILL, Gtk::SHRINK, 2, 2)
3168 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)
3170 smp_check.signal_connect('toggled') {
3171 if smp_check.active?
3172 smp_hbox.sensitive = true
3174 smp_hbox.sensitive = false
3178 smp_check.active = true
3179 smp_spin.value = $config['mproc'].to_i
3181 nogestures_check.active = $config['nogestures']
3182 deleteondisk_check.active = $config['deleteondisk']
3184 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3185 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3186 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3187 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3188 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3189 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3190 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3191 tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3192 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3193 tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3194 2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3195 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)
3196 commentsformat_help.signal_connect('clicked') {
3197 show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3198 hence you should look at ImageMagick/identify documentation for the most
3199 accurate and up-to-date documentation. Last time I checked, documentation
3202 Print information about the image in a format of your choosing. You can
3203 include the image filename, type, width, height, Exif data, or other image
3204 attributes by embedding special format characters:
3207 %P page width and height
3211 %e filename extension
3216 %k number of unique colors
3223 %r image class and colorspace
3226 %u unique temporary filename
3239 displays MIFF:bird.miff 512x480 for an image titled bird.miff and whose
3240 width is 512 and height is 480.
3242 If the first character of string is @, the format is read from a file titled
3243 by the remaining characters in the string.
3245 You can also use the following special formatting syntax to print Exif
3246 information contained in the file:
3250 Where tag can be one of the following:
3252 * (print all Exif tags, in keyword=data format)
3253 ! (print all Exif tags, in tag_number data format)
3254 #hhhh (print data for Exif tag #hhhh)
3259 PhotometricInterpretation
3279 PrimaryChromaticities
3282 JPEGInterchangeFormat
3283 JPEGInterchangeFormatLength
3305 ComponentsConfiguration
3306 CompressedBitsPerPixel
3326 InteroperabilityOffset
3328 SpatialFrequencyResponse
3329 FocalPlaneXResolution
3330 FocalPlaneYResolution
3331 FocalPlaneResolutionUnit
3336 SceneType")), { :scrolled => true })
3339 dialog.vbox.show_all
3340 dialog.run { |response|
3341 if response == Gtk::Dialog::RESPONSE_OK
3342 $config['video-viewer'] = video_viewer_entry.text
3343 $config['browser'] = browser_entry.text
3344 if smp_check.active?
3345 $config['mproc'] = smp_spin.value.to_i
3347 $config.delete('mproc')
3349 $config['nogestures'] = nogestures_check.active?
3350 $config['deleteondisk'] = deleteondisk_check.active?
3352 $config['convert-enhance'] = enhance_entry.text
3353 $config['comments-format'] = commentsformat_entry.text.gsub(/'/, '')
3360 if $undo_tb.sensitive?
3361 $redo_tb.sensitive = $redo_mb.sensitive = true
3362 if not more_undoes = UndoHandler.undo($statusbar)
3363 $undo_tb.sensitive = $undo_mb.sensitive = false
3369 if $redo_tb.sensitive?
3370 $undo_tb.sensitive = $undo_mb.sensitive = true
3371 if not more_redoes = UndoHandler.redo($statusbar)
3372 $redo_tb.sensitive = $redo_mb.sensitive = false
3377 def show_one_click_explanation(intro)
3378 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
3380 %s When such a tool is activated
3381 (<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
3382 on a thumbnail will immediately apply the desired action.
3384 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
3385 ") % intro), { :pos_centered => true })
3390 GNU GENERAL PUBLIC LICENSE
3391 Version 2, June 1991
3393 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
3394 675 Mass Ave, Cambridge, MA 02139, USA
3395 Everyone is permitted to copy and distribute verbatim copies
3396 of this license document, but changing it is not allowed.
3400 The licenses for most software are designed to take away your
3401 freedom to share and change it. By contrast, the GNU General Public
3402 License is intended to guarantee your freedom to share and change free
3403 software--to make sure the software is free for all its users. This
3404 General Public License applies to most of the Free Software
3405 Foundation's software and to any other program whose authors commit to
3406 using it. (Some other Free Software Foundation software is covered by
3407 the GNU Library General Public License instead.) You can apply it to
3410 When we speak of free software, we are referring to freedom, not
3411 price. Our General Public Licenses are designed to make sure that you
3412 have the freedom to distribute copies of free software (and charge for
3413 this service if you wish), that you receive source code or can get it
3414 if you want it, that you can change the software or use pieces of it
3415 in new free programs; and that you know you can do these things.
3417 To protect your rights, we need to make restrictions that forbid
3418 anyone to deny you these rights or to ask you to surrender the rights.
3419 These restrictions translate to certain responsibilities for you if you
3420 distribute copies of the software, or if you modify it.
3422 For example, if you distribute copies of such a program, whether
3423 gratis or for a fee, you must give the recipients all the rights that
3424 you have. You must make sure that they, too, receive or can get the
3425 source code. And you must show them these terms so they know their
3428 We protect your rights with two steps: (1) copyright the software, and
3429 (2) offer you this license which gives you legal permission to copy,
3430 distribute and/or modify the software.
3432 Also, for each author's protection and ours, we want to make certain
3433 that everyone understands that there is no warranty for this free
3434 software. If the software is modified by someone else and passed on, we
3435 want its recipients to know that what they have is not the original, so
3436 that any problems introduced by others will not reflect on the original
3437 authors' reputations.
3439 Finally, any free program is threatened constantly by software
3440 patents. We wish to avoid the danger that redistributors of a free
3441 program will individually obtain patent licenses, in effect making the
3442 program proprietary. To prevent this, we have made it clear that any
3443 patent must be licensed for everyone's free use or not licensed at all.
3445 The precise terms and conditions for copying, distribution and
3446 modification follow.
3449 GNU GENERAL PUBLIC LICENSE
3450 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
3452 0. This License applies to any program or other work which contains
3453 a notice placed by the copyright holder saying it may be distributed
3454 under the terms of this General Public License. The "Program", below,
3455 refers to any such program or work, and a "work based on the Program"
3456 means either the Program or any derivative work under copyright law:
3457 that is to say, a work containing the Program or a portion of it,
3458 either verbatim or with modifications and/or translated into another
3459 language. (Hereinafter, translation is included without limitation in
3460 the term "modification".) Each licensee is addressed as "you".
3462 Activities other than copying, distribution and modification are not
3463 covered by this License; they are outside its scope. The act of
3464 running the Program is not restricted, and the output from the Program
3465 is covered only if its contents constitute a work based on the
3466 Program (independent of having been made by running the Program).
3467 Whether that is true depends on what the Program does.
3469 1. You may copy and distribute verbatim copies of the Program's
3470 source code as you receive it, in any medium, provided that you
3471 conspicuously and appropriately publish on each copy an appropriate
3472 copyright notice and disclaimer of warranty; keep intact all the
3473 notices that refer to this License and to the absence of any warranty;
3474 and give any other recipients of the Program a copy of this License
3475 along with the Program.
3477 You may charge a fee for the physical act of transferring a copy, and
3478 you may at your option offer warranty protection in exchange for a fee.
3480 2. You may modify your copy or copies of the Program or any portion
3481 of it, thus forming a work based on the Program, and copy and
3482 distribute such modifications or work under the terms of Section 1
3483 above, provided that you also meet all of these conditions:
3485 a) You must cause the modified files to carry prominent notices
3486 stating that you changed the files and the date of any change.
3488 b) You must cause any work that you distribute or publish, that in
3489 whole or in part contains or is derived from the Program or any
3490 part thereof, to be licensed as a whole at no charge to all third
3491 parties under the terms of this License.
3493 c) If the modified program normally reads commands interactively
3494 when run, you must cause it, when started running for such
3495 interactive use in the most ordinary way, to print or display an
3496 announcement including an appropriate copyright notice and a
3497 notice that there is no warranty (or else, saying that you provide
3498 a warranty) and that users may redistribute the program under
3499 these conditions, and telling the user how to view a copy of this
3500 License. (Exception: if the Program itself is interactive but
3501 does not normally print such an announcement, your work based on
3502 the Program is not required to print an announcement.)
3505 These requirements apply to the modified work as a whole. If
3506 identifiable sections of that work are not derived from the Program,
3507 and can be reasonably considered independent and separate works in
3508 themselves, then this License, and its terms, do not apply to those
3509 sections when you distribute them as separate works. But when you
3510 distribute the same sections as part of a whole which is a work based
3511 on the Program, the distribution of the whole must be on the terms of
3512 this License, whose permissions for other licensees extend to the
3513 entire whole, and thus to each and every part regardless of who wrote it.
3515 Thus, it is not the intent of this section to claim rights or contest
3516 your rights to work written entirely by you; rather, the intent is to
3517 exercise the right to control the distribution of derivative or
3518 collective works based on the Program.
3520 In addition, mere aggregation of another work not based on the Program
3521 with the Program (or with a work based on the Program) on a volume of
3522 a storage or distribution medium does not bring the other work under
3523 the scope of this License.
3525 3. You may copy and distribute the Program (or a work based on it,
3526 under Section 2) in object code or executable form under the terms of
3527 Sections 1 and 2 above provided that you also do one of the following:
3529 a) Accompany it with the complete corresponding machine-readable
3530 source code, which must be distributed under the terms of Sections
3531 1 and 2 above on a medium customarily used for software interchange; or,
3533 b) Accompany it with a written offer, valid for at least three
3534 years, to give any third party, for a charge no more than your
3535 cost of physically performing source distribution, a complete
3536 machine-readable copy of the corresponding source code, to be
3537 distributed under the terms of Sections 1 and 2 above on a medium
3538 customarily used for software interchange; or,
3540 c) Accompany it with the information you received as to the offer
3541 to distribute corresponding source code. (This alternative is
3542 allowed only for noncommercial distribution and only if you
3543 received the program in object code or executable form with such
3544 an offer, in accord with Subsection b above.)
3546 The source code for a work means the preferred form of the work for
3547 making modifications to it. For an executable work, complete source
3548 code means all the source code for all modules it contains, plus any
3549 associated interface definition files, plus the scripts used to
3550 control compilation and installation of the executable. However, as a
3551 special exception, the source code distributed need not include
3552 anything that is normally distributed (in either source or binary
3553 form) with the major components (compiler, kernel, and so on) of the
3554 operating system on which the executable runs, unless that component
3555 itself accompanies the executable.
3557 If distribution of executable or object code is made by offering
3558 access to copy from a designated place, then offering equivalent
3559 access to copy the source code from the same place counts as
3560 distribution of the source code, even though third parties are not
3561 compelled to copy the source along with the object code.
3564 4. You may not copy, modify, sublicense, or distribute the Program
3565 except as expressly provided under this License. Any attempt
3566 otherwise to copy, modify, sublicense or distribute the Program is
3567 void, and will automatically terminate your rights under this License.
3568 However, parties who have received copies, or rights, from you under
3569 this License will not have their licenses terminated so long as such
3570 parties remain in full compliance.
3572 5. You are not required to accept this License, since you have not
3573 signed it. However, nothing else grants you permission to modify or
3574 distribute the Program or its derivative works. These actions are
3575 prohibited by law if you do not accept this License. Therefore, by
3576 modifying or distributing the Program (or any work based on the
3577 Program), you indicate your acceptance of this License to do so, and
3578 all its terms and conditions for copying, distributing or modifying
3579 the Program or works based on it.
3581 6. Each time you redistribute the Program (or any work based on the
3582 Program), the recipient automatically receives a license from the
3583 original licensor to copy, distribute or modify the Program subject to
3584 these terms and conditions. You may not impose any further
3585 restrictions on the recipients' exercise of the rights granted herein.
3586 You are not responsible for enforcing compliance by third parties to
3589 7. If, as a consequence of a court judgment or allegation of patent
3590 infringement or for any other reason (not limited to patent issues),
3591 conditions are imposed on you (whether by court order, agreement or
3592 otherwise) that contradict the conditions of this License, they do not
3593 excuse you from the conditions of this License. If you cannot
3594 distribute so as to satisfy simultaneously your obligations under this
3595 License and any other pertinent obligations, then as a consequence you
3596 may not distribute the Program at all. For example, if a patent
3597 license would not permit royalty-free redistribution of the Program by
3598 all those who receive copies directly or indirectly through you, then
3599 the only way you could satisfy both it and this License would be to
3600 refrain entirely from distribution of the Program.
3602 If any portion of this section is held invalid or unenforceable under
3603 any particular circumstance, the balance of the section is intended to
3604 apply and the section as a whole is intended to apply in other
3607 It is not the purpose of this section to induce you to infringe any
3608 patents or other property right claims or to contest validity of any
3609 such claims; this section has the sole purpose of protecting the
3610 integrity of the free software distribution system, which is
3611 implemented by public license practices. Many people have made
3612 generous contributions to the wide range of software distributed
3613 through that system in reliance on consistent application of that
3614 system; it is up to the author/donor to decide if he or she is willing
3615 to distribute software through any other system and a licensee cannot
3618 This section is intended to make thoroughly clear what is believed to
3619 be a consequence of the rest of this License.
3622 8. If the distribution and/or use of the Program is restricted in
3623 certain countries either by patents or by copyrighted interfaces, the
3624 original copyright holder who places the Program under this License
3625 may add an explicit geographical distribution limitation excluding
3626 those countries, so that distribution is permitted only in or among
3627 countries not thus excluded. In such case, this License incorporates
3628 the limitation as if written in the body of this License.
3630 9. The Free Software Foundation may publish revised and/or new versions
3631 of the General Public License from time to time. Such new versions will
3632 be similar in spirit to the present version, but may differ in detail to
3633 address new problems or concerns.
3635 Each version is given a distinguishing version number. If the Program
3636 specifies a version number of this License which applies to it and "any
3637 later version", you have the option of following the terms and conditions
3638 either of that version or of any later version published by the Free
3639 Software Foundation. If the Program does not specify a version number of
3640 this License, you may choose any version ever published by the Free Software
3643 10. If you wish to incorporate parts of the Program into other free
3644 programs whose distribution conditions are different, write to the author
3645 to ask for permission. For software which is copyrighted by the Free
3646 Software Foundation, write to the Free Software Foundation; we sometimes
3647 make exceptions for this. Our decision will be guided by the two goals
3648 of preserving the free status of all derivatives of our free software and
3649 of promoting the sharing and reuse of software generally.
3653 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
3654 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
3655 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
3656 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
3657 OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
3658 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
3659 TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
3660 PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
3661 REPAIR OR CORRECTION.
3663 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
3664 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
3665 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
3666 INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
3667 OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
3668 TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
3669 YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
3670 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
3671 POSSIBILITY OF SUCH DAMAGES.
3675 def create_menu_and_toolbar
3678 mb = Gtk::MenuBar.new
3680 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
3681 filesubmenu = Gtk::Menu.new
3682 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
3683 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
3684 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3685 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
3686 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
3687 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3688 tooltips = Gtk::Tooltips.new
3689 filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
3690 $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3691 tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
3692 filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
3693 $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3694 tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
3695 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
3696 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3697 tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums (anywhere)")), nil)
3698 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3699 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
3700 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3701 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
3702 filesubmenu.append($view_wa = Gtk::ImageMenuItem.new(utf8(_("View web-album with browser"))).set_sensitive(false))
3703 $view_wa.image = Gtk::Image.new("#{$FPATH}/images/stock-view-webalbum-16.png")
3704 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3705 filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
3706 tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
3707 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3708 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
3709 filemenu.set_submenu(filesubmenu)
3712 new.signal_connect('activate') { new_album }
3713 open.signal_connect('activate') { open_file_popup }
3714 $save.signal_connect('activate') { save_current_file_user }
3715 $save_as.signal_connect('activate') { save_as_do }
3716 $merge_current.signal_connect('activate') { merge_current }
3717 $merge_newsubs.signal_connect('activate') { merge_newsubs }
3718 $merge.signal_connect('activate') { merge }
3719 $generate.signal_connect('activate') {
3721 call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
3722 utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
3724 { :successmsg => utf8(_("Your web-album is now ready in directory '%s'.
3725 Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
3726 :successmsg_linkurl => $xmldoc.root.attributes['destination'],
3727 :closure_after => proc {
3728 $xmldoc.elements.each('//dir') { |elem|
3729 $modified ||= elem.attributes['already-generated'].nil?
3730 elem.add_attribute('already-generated', 'true')
3732 UndoHandler.cleanup #- prevent save_changes to mark current dir as not already generated
3733 $undo_tb.sensitive = $undo_mb.sensitive = false
3734 $redo_tb.sensitive = $redo_mb.sensitive = false
3736 $generated_outofline = true
3739 $view_wa.signal_connect('activate') {
3740 indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
3741 if File.exists?(indexhtml)
3744 show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
3747 $properties.signal_connect('activate') { properties }
3749 quit.signal_connect('activate') { try_quit }
3751 editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
3752 editsubmenu = Gtk::Menu.new
3753 editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
3754 editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
3755 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3756 editsubmenu.append($sort_by_exif_date = Gtk::ImageMenuItem.new(utf8(_("Sort by EXIF date"))).set_sensitive(false))
3757 $sort_by_exif_date.image = Gtk::Image.new("#{$FPATH}/images/sort_by_exif_date.png")
3758 editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
3759 $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
3760 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)
3761 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3762 editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
3763 editmenu.set_submenu(editsubmenu)
3766 $remove_all_captions.signal_connect('activate') { remove_all_captions }
3767 $sort_by_exif_date.signal_connect('activate') { sort_by_exif_date }
3769 prefs.signal_connect('activate') { preferences }
3771 helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
3772 helpsubmenu = Gtk::Menu.new
3773 helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
3774 one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
3775 helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
3776 speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
3777 helpsubmenu.append(tutos = Gtk::ImageMenuItem.new(utf8(_("Online tutorials (opens a web-browser)"))))
3778 tutos.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3779 helpsubmenu.append(Gtk::SeparatorMenuItem.new)
3780 helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
3781 helpmenu.set_submenu(helpsubmenu)
3784 one_click.signal_connect('activate') {
3785 show_one_click_explanation(_("One-Click tools are available in the toolbar."))
3788 speed.signal_connect('activate') {
3789 show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
3791 <span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
3792 <span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
3793 <span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
3794 <span foreground='darkblue'>Control-Enter</span>: for an image, open larger view; for a video, launch player
3795 <span foreground='darkblue'>Control-Delete</span>: delete image
3796 <span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
3797 <span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
3798 <span foreground='darkblue'>Control-z</span>: undo
3799 <span foreground='darkblue'>Control-r</span>: redo
3801 <span size='large' weight='bold'>Mouse gestures:</span>
3803 Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
3804 for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
3806 <span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
3807 <span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
3808 <span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
3809 <span foreground='darkblue'>Left click, hold left button, right click</span>: undo
3810 <span foreground='darkblue'>Right click, hold right button, left click</span>: redo
3811 ")), { :pos_centered => true, :not_transient => true })
3814 tutos.signal_connect('activate') {
3815 open_url('http://zarb.org/~gc/html/booh/tutorial.html')
3818 about.signal_connect('activate') {
3819 Gtk::AboutDialog.set_url_hook { |dialog, url| open_url(url) }
3820 Gtk::AboutDialog.show($main_window, { :name => 'booh',
3821 :version => $VERSION,
3822 :copyright => 'Copyright (c) 2005 Guillaume Cottenceau',
3823 :license => get_license,
3824 :website => 'http://zarb.org/~gc/html/booh.html',
3825 :authors => [ 'Guillaume Cottenceau' ],
3826 :artists => [ 'Ayo73' ],
3827 :comments => utf8(_("''The Web-Album of choice for discriminating Linux users''")),
3828 :translator_credits => utf8(_('Japanese: Masao Mutoh
3829 German: Roland Eckert
3830 French: Guillaume Cottenceau')),
3831 :logo => Gdk::Pixbuf.new("#{$FPATH}/images/logo.png") })
3836 tb = Gtk::Toolbar.new
3838 tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
3839 open.label = utf8(_("Open")) #- to avoid missing gtk2 l10n catalogs
3840 open.menu = Gtk::Menu.new
3841 open.signal_connect('clicked') { open_file_popup }
3842 open.signal_connect('show-menu') {
3843 lastopens = Gtk::Menu.new
3845 if $config['last-opens']
3846 $config['last-opens'].reverse.each { |e|
3847 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
3848 item.signal_connect('activate') {
3849 if ask_save_modifications(utf8(_("Save this album?")),
3850 utf8(_("Do you want to save the changes to this album?")),
3851 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3852 push_mousecursor_wait
3853 msg = open_file_user(from_utf8(e))
3856 show_popup($main_window, msg)
3864 open.menu = lastopens
3867 tb.insert(-1, Gtk::SeparatorToolItem.new)
3869 tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
3870 $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
3871 $r90.label = utf8(_("Rotate"))
3872 tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
3873 $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
3874 $r270.label = utf8(_("Rotate"))
3875 tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
3876 $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
3877 $enhance.label = utf8(_("Enhance"))
3878 tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
3879 $delete.label = utf8(_("Delete")) #- to avoid missing gtk2 l10n catalogs
3880 tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
3881 nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
3882 nothing.label = utf8(_("None"))
3884 tb.insert(-1, Gtk::SeparatorToolItem.new)
3886 tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
3887 tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
3890 $undo_tb.signal_connect('clicked') { perform_undo }
3891 $undo_mb.signal_connect('activate') { perform_undo }
3892 $redo_tb.signal_connect('clicked') { perform_redo }
3893 $redo_mb.signal_connect('activate') { perform_redo }
3895 one_click_explain_try = proc {
3896 if !$config['one-click-explained']
3897 show_one_click_explanation(_("You have just clicked on a One-Click tool."))
3898 $config['one-click-explained'] = true
3902 $r90.signal_connect('toggled') {
3904 set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
3905 one_click_explain_try.call
3906 $r270.active = false
3907 $enhance.active = false
3908 $delete.active = false
3909 nothing.sensitive = true
3911 if !$r270.active? && !$enhance.active? && !$delete.active?
3912 set_mousecursor_normal
3913 nothing.sensitive = false
3915 nothing.sensitive = true
3919 $r270.signal_connect('toggled') {
3921 set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
3922 one_click_explain_try.call
3924 $enhance.active = false
3925 $delete.active = false
3926 nothing.sensitive = true
3928 if !$r90.active? && !$enhance.active? && !$delete.active?
3929 set_mousecursor_normal
3930 nothing.sensitive = false
3932 nothing.sensitive = true
3936 $enhance.signal_connect('toggled') {
3938 set_mousecursor(Gdk::Cursor::SPRAYCAN)
3939 one_click_explain_try.call
3941 $r270.active = false
3942 $delete.active = false
3943 nothing.sensitive = true
3945 if !$r90.active? && !$r270.active? && !$delete.active?
3946 set_mousecursor_normal
3947 nothing.sensitive = false
3949 nothing.sensitive = true
3953 $delete.signal_connect('toggled') {
3955 set_mousecursor(Gdk::Cursor::PIRATE)
3956 one_click_explain_try.call
3958 $r270.active = false
3959 $enhance.active = false
3960 nothing.sensitive = true
3962 if !$r90.active? && !$r270.active? && !$enhance.active?
3963 set_mousecursor_normal
3964 nothing.sensitive = false
3966 nothing.sensitive = true
3970 nothing.signal_connect('clicked') {
3971 $r90.active = $r270.active = $enhance.active = $delete.active = false
3972 set_mousecursor_normal
3978 def gtk_thread_protect(&proc)
3979 if Thread.current == Thread.main
3982 $protect_gtk_pending_calls.synchronize {
3983 $gtk_pending_calls << proc
3988 def gtk_thread_flush
3989 $protect_gtk_pending_calls.try_lock
3990 for closure in $gtk_pending_calls
3993 $gtk_pending_calls = []
3994 $protect_gtk_pending_calls.unlock
3997 def ask_password_protect
3998 value = $xmldir.attributes['password-protect']
4000 dialog = Gtk::Dialog.new(utf8(_("Password protect this sub-album")),
4002 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
4003 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
4004 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
4006 lbl = Gtk::Label.new