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 if !FileTest.directory?(File.expand_path('~/.booh'))
113 system("mkdir ~/.booh")
121 if !system("which convert >/dev/null 2>/dev/null")
122 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
123 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
126 if !system("which identify >/dev/null 2>/dev/null")
127 show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
128 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
130 missing = %w(transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
132 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
135 viewer_binary = $config['video-viewer'].split.first
136 if viewer_binary && !File.executable?(viewer_binary)
137 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
138 You should fix this in Edit/Preferences so that you can view videos.
140 Problem was: '%s' is not an executable file.
141 Hint: don't forget to specify the full path to the executable,
142 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
144 browser_binary = $config['browser'].split.first
145 if browser_binary && !File.executable?(browser_binary)
146 show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
147 You should fix this in Edit/Preferences so that you can open URLs.
149 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
154 if $config['last-opens'] && $config['last-opens'].size > 5
155 $config['last-opens'] = $config['last-opens'][-5, 5]
158 ios = File.open($config_file, "w")
159 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
160 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
161 $config.each_pair { |key, value|
162 elem = $xmldoc.root.add_element key
164 $config[key].each_pair { |subkey, subvalue|
165 subelem = elem.add_element subkey
166 subelem.add_text subvalue.to_s
168 elsif value.is_a? Array
169 elem.add_text value.join('~~~')
174 elem.add_text value.to_s
178 $xmldoc.write(ios, 0)
181 $tempfiles.each { |f|
186 def set_mousecursor(what, *widget)
187 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
188 if widget[0] && widget[0].window
189 widget[0].window.cursor = cursor
191 if $main_window.window
192 $main_window.window.cursor = cursor
194 $current_cursor = what
196 def set_mousecursor_wait(*widget)
197 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
198 if Thread.current == Thread.main
199 Gtk.main_iteration while Gtk.events_pending?
202 def set_mousecursor_normal(*widget)
203 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
205 def push_mousecursor_wait(*widget)
206 if $current_cursor != Gdk::Cursor::WATCH
207 $save_cursor = $current_cursor
208 gtk_thread_protect { set_mousecursor_wait(*widget) }
211 def pop_mousecursor(*widget)
212 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
216 source = $xmldoc.root.attributes['source']
217 dest = $xmldoc.root.attributes['destination']
218 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
221 def full_src_dir_to_rel(path, source)
222 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
225 def build_full_dest_filename(filename)
226 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
229 def save_undo(name, closure, *params)
230 UndoHandler.save_undo(name, closure, [ *params ])
231 $undo_tb.sensitive = $undo_mb.sensitive = true
232 $redo_tb.sensitive = $redo_mb.sensitive = false
235 def view_element(filename, closures)
236 if entry2type(filename) == 'video'
237 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
243 w = Gtk::Window.new.set_title(filename)
245 msg 3, "filename: #{filename}"
246 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
247 #- typically this file won't exist in case of videos; try with the largest thumbnail around
248 if !File.exists?(dest_img)
249 if entry2type(filename) == 'video'
250 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
251 if not alternatives.empty?
252 dest_img = alternatives[-1]
255 push_mousecursor_wait
256 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
258 if !File.exists?(dest_img)
259 msg 2, _("Could not generate fullscreen thumbnail!")
264 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)))
265 evt.signal_connect('button-press-event') { |this, event|
266 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
267 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
269 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
271 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
272 delete_item.signal_connect('activate') {
274 closures[:delete].call
277 menu.popup(nil, nil, event.button, event.time)
280 evt.signal_connect('button-release-event') { |this, event|
282 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
283 msg 3, "gesture delete: click-drag right button to the bottom"
285 closures[:delete].call
286 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
290 tooltips = Gtk::Tooltips.new
291 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
293 w.signal_connect('key-press-event') { |w,event|
294 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
296 closures[:delete].call
300 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
301 b.signal_connect('clicked') { w.destroy }
304 vb.pack_start(evt, false, false)
305 vb.pack_end(bottom, false, false)
308 w.signal_connect('delete-event') { w.destroy }
309 w.window_position = Gtk::Window::POS_CENTER
313 def scroll_upper(scrolledwindow, ypos_top)
314 newval = scrolledwindow.vadjustment.value -
315 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
316 if newval < scrolledwindow.vadjustment.lower
317 newval = scrolledwindow.vadjustment.lower
319 scrolledwindow.vadjustment.value = newval
322 def scroll_lower(scrolledwindow, ypos_bottom)
323 newval = scrolledwindow.vadjustment.value +
324 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
325 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
326 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
328 scrolledwindow.vadjustment.value = newval
331 def autoscroll_if_needed(scrolledwindow, image, textview)
332 #- autoscroll if cursor or image is not visible, if possible
333 if image && image.window || textview.window
334 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
335 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
336 current_miny_visible = scrolledwindow.vadjustment.value
337 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
338 if ypos_top < current_miny_visible
339 scroll_upper(scrolledwindow, ypos_top)
340 elsif ypos_bottom > current_maxy_visible
341 scroll_lower(scrolledwindow, ypos_bottom)
346 def create_editzone(scrolledwindow, pagenum, image)
347 frame = Gtk::Frame.new
348 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
349 frame.set_shadow_type(Gtk::SHADOW_IN)
350 textview.signal_connect('key-press-event') { |w, event|
351 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
352 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
353 scrolledwindow.signal_emit('key-press-event', event)
355 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
356 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
357 if event.keyval == Gdk::Keyval::GDK_Up
358 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
359 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
361 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
364 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
365 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
367 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
373 textview.signal_connect('focus-in-event') { |w, event|
374 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
378 candidate_undo_text = nil
379 textview.signal_connect('focus-in-event') { |w, event|
380 candidate_undo_text = textview.buffer.text
383 textview.signal_connect('key-release-event') { |w, event|
384 if candidate_undo_text && candidate_undo_text != textview.buffer.text
386 save_undo(_("text edit"),
388 save_text = textview.buffer.text
389 textview.buffer.text = text
391 $notebook.set_page(pagenum)
393 textview.buffer.text = save_text
395 $notebook.set_page(pagenum)
397 }, candidate_undo_text)
398 candidate_undo_text = nil
401 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)
402 autoscroll_if_needed(scrolledwindow, image, textview)
407 return [ frame, textview ]
410 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
412 if !$modified_pixbufs[thumbnail_img]
413 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
414 elsif !$modified_pixbufs[thumbnail_img][:orig]
415 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
418 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
421 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
422 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
423 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
424 if pixbuf.height > desired_y
425 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
426 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
427 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
432 if $modified_pixbufs[thumbnail_img][:whitebalance]
433 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
436 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
439 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
442 #- update rotate attribute
443 xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
445 $modified_pixbufs[thumbnail_img] ||= {}
446 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
447 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
449 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
452 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
455 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
457 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
459 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
460 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
462 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
463 $notebook.set_page(0)
464 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
469 def color_swap(xmldir, attributes_prefix)
471 if xmldir.attributes["#{attributes_prefix}color-swap"]
472 xmldir.delete_attribute("#{attributes_prefix}color-swap")
474 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
478 def enhance(xmldir, attributes_prefix)
480 if xmldir.attributes["#{attributes_prefix}enhance"]
481 xmldir.delete_attribute("#{attributes_prefix}enhance")
483 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
487 def change_frame_offset(xmldir, attributes_prefix, value)
489 xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
492 def ask_new_frame_offset(xmldir, attributes_prefix)
494 value = xmldir.attributes["#{attributes_prefix}frame-offset"]
499 dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
501 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
502 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
503 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
507 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
508 from. There are approximately 25 frames per second in a video.
511 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
512 entry.signal_connect('key-press-event') { |w, event|
513 if event.keyval == Gdk::Keyval::GDK_Return
514 dialog.response(Gtk::Dialog::RESPONSE_OK)
516 elsif event.keyval == Gdk::Keyval::GDK_Escape
517 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
520 false #- propagate if needed
524 dialog.window_position = Gtk::Window::POS_MOUSE
527 dialog.run { |response|
530 if response == Gtk::Dialog::RESPONSE_OK
532 msg 3, "changing frame offset to #{newval}"
533 return { :old => value, :new => newval }
540 def change_pano_amount(xmldir, attributes_prefix, value)
543 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
545 xmldir.add_attribute("#{attributes_prefix}pano-amount", value)
549 def ask_new_pano_amount(xmldir, attributes_prefix)
551 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
556 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
558 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
559 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
560 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
564 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
565 of this panorama image compared to other regular images. For example, if the panorama
566 was taken out of four photos on one row, counting the necessary overlap, the width of
567 this panorama image should probably be roughly three times the width of regular images.
569 With this information, booh will be able to generate panorama thumbnails looking
573 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)")))).
574 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
575 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
576 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
577 dialog.window_position = Gtk::Window::POS_MOUSE
580 spin.value = value.to_f
587 dialog.run { |response|
591 newval = spin.value.to_f
594 if response == Gtk::Dialog::RESPONSE_OK
596 msg 3, "changing panorama amount to #{newval}"
597 return { :old => value, :new => newval }
604 def change_whitebalance(xmlelem, attributes_prefix, value)
606 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
609 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
611 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
612 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
613 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
614 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
615 destfile = "#{thumbnail_img}-orig-whitebalance.jpg"
616 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
617 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
618 $modified_pixbufs[thumbnail_img] ||= {}
619 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
620 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
621 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
624 $modified_pixbufs[thumbnail_img] ||= {}
625 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
627 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
630 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
631 #- init $modified_pixbufs correctly
632 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
634 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
636 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
638 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
639 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
640 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
644 _("You can fix the <b>white balance</b> of the image, if your image is too blue
645 or too yellow because your camera didn't detect the light correctly. Drag the
646 slider below the image to the left for more blue, to the right for more yellow.
650 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
652 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
654 dialog.window_position = Gtk::Window::POS_MOUSE
658 timeout = Gtk.timeout_add(100) {
659 if hs.value != lastval
662 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
668 dialog.run { |response|
669 Gtk.timeout_remove(timeout)
670 if response == Gtk::Dialog::RESPONSE_OK
672 newval = hs.value.to_s
673 msg 3, "changing white balance to #{newval}"
675 return { :old => value, :new => newval }
678 $modified_pixbufs[thumbnail_img] ||= {}
679 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
680 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
688 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
689 system("rm -f '#{destfile}'")
690 #- type can be 'element' or 'subdir'
692 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
694 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
698 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
700 push_mousecursor_wait
701 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
703 puts "destroyed: " + img.destroyed?.to_s
705 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
711 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
712 distribute_multiple_call = Proc.new { |action, arg|
713 $selected_elements.each_key { |path|
714 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
716 if possible_actions[:can_multiple] && $selected_elements.length > 0
717 UndoHandler.begin_batch
718 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
719 UndoHandler.end_batch
721 closures[action].call(arg)
723 $selected_elements = {}
726 if optionals.include?('change_image')
727 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
728 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
729 changeimg.signal_connect('activate') { closures[:change].call }
730 menu.append(Gtk::SeparatorMenuItem.new)
732 if !possible_actions[:can_multiple] || $selected_elements.length == 0
735 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
736 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
737 view.signal_connect('activate') { closures[:view].call }
739 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
740 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
741 view.signal_connect('activate') { closures[:view].call }
742 menu.append(Gtk::SeparatorMenuItem.new)
745 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
746 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
747 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
748 exif.signal_connect('activate') { show_popup($main_window,
749 utf8(`identify -format "%[EXIF:*]" #{fullpath}`.sub(/MakerNote.*\n/, '')),
750 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
751 menu.append(Gtk::SeparatorMenuItem.new)
754 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
755 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
756 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
757 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
758 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
759 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
760 if !possible_actions[:can_multiple] || $selected_elements.length == 0
761 menu.append(Gtk::SeparatorMenuItem.new)
762 if !possible_actions[:forbid_left]
763 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
764 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
765 moveleft.signal_connect('activate') { closures[:move].call('left') }
766 if !possible_actions[:can_left]
767 moveleft.sensitive = false
770 if !possible_actions[:forbid_right]
771 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
772 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
773 moveright.signal_connect('activate') { closures[:move].call('right') }
774 if !possible_actions[:can_right]
775 moveright.sensitive = false
778 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
779 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
780 moveup.signal_connect('activate') { closures[:move].call('up') }
781 if !possible_actions[:can_up]
782 moveup.sensitive = false
784 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
785 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
786 movedown.signal_connect('activate') { closures[:move].call('down') }
787 if !possible_actions[:can_down]
788 movedown.sensitive = false
792 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
793 menu.append(Gtk::SeparatorMenuItem.new)
794 menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
795 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
796 color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
797 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
798 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
799 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
800 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
801 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
802 frame_offset.signal_connect('activate') {
803 if possible_actions[:can_multiple] && $selected_elements.length > 0
804 if values = ask_new_frame_offset(nil, '')
805 distribute_multiple_call.call(:frame_offset, values)
808 closures[:frame_offset].call
813 menu.append( Gtk::SeparatorMenuItem.new)
814 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
815 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
816 whitebalance.signal_connect('activate') {
817 if possible_actions[:can_multiple] && $selected_elements.length > 0
818 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
819 distribute_multiple_call.call(:whitebalance, values)
822 closures[:whitebalance].call
825 if !possible_actions[:can_multiple] || $selected_elements.length == 0
826 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
827 _("Enhance constrast"))))
829 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
831 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
832 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
833 if type == 'image' && possible_actions[:can_panorama]
834 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
835 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
836 panorama.signal_connect('activate') {
837 if possible_actions[:can_multiple] && $selected_elements.length > 0
838 if values = ask_new_pano_amount(nil, '')
839 distribute_multiple_call.call(:pano, values)
842 distribute_multiple_call.call(:pano)
846 if optionals.include?('delete')
847 menu.append( Gtk::SeparatorMenuItem.new)
848 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
849 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
850 if !possible_actions[:can_multiple] || $selected_elements.length == 0
851 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
852 paste_item.signal_connect('activate') { closures[:paste].call }
853 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
854 clear_item.signal_connect('activate') { $cuts = [] }
856 paste_item.sensitive = clear_item.sensitive = false
859 menu.append( Gtk::SeparatorMenuItem.new)
860 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
861 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
864 menu.popup(nil, nil, event.button, event.time)
867 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
870 frame1 = Gtk::Frame.new
871 fullpath = from_utf8("#{$current_path}/#{filename}")
873 my_gen_real_thumbnail = proc {
874 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
877 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
878 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
879 frame1.add(img = Gtk::Image.new)
880 my_gen_real_thumbnail.call
882 frame1.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
884 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
886 tooltips = Gtk::Tooltips.new
887 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
888 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
890 frame2, textview = create_editzone($autotable_sw, 1, img)
891 textview.buffer.text = utf8(caption)
892 textview.set_justification(Gtk::Justification::CENTER)
894 vbox = Gtk::VBox.new(false, 5)
895 vbox.pack_start(evtbox, false, false)
896 vbox.pack_start(frame2, false, false)
897 autotable.append(vbox, filename)
899 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
900 $vbox2widgets[vbox] = { :textview => textview, :image => img }
902 #- to be able to find widgets by name
903 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
905 cleanup_all_thumbnails = Proc.new {
906 #- remove out of sync images
907 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
908 for sizeobj in $images_size
909 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
914 rotate_and_cleanup = Proc.new { |angle|
915 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
916 cleanup_all_thumbnails.call
919 move = Proc.new { |direction|
920 do_method = "move_#{direction}"
921 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
923 done = autotable.method(do_method).call(vbox)
924 textview.grab_focus #- because if moving, focus is stolen
928 save_undo(_("move %s") % direction,
930 autotable.method(undo_method).call(vbox)
931 textview.grab_focus #- because if moving, focus is stolen
932 autoscroll_if_needed($autotable_sw, img, textview)
933 $notebook.set_page(1)
935 autotable.method(do_method).call(vbox)
936 textview.grab_focus #- because if moving, focus is stolen
937 autoscroll_if_needed($autotable_sw, img, textview)
938 $notebook.set_page(1)
944 color_swap_and_cleanup = Proc.new {
945 perform_color_swap_and_cleanup = Proc.new {
946 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
947 my_gen_real_thumbnail.call
950 cleanup_all_thumbnails.call
951 perform_color_swap_and_cleanup.call
953 save_undo(_("color swap"),
955 perform_color_swap_and_cleanup.call
957 autoscroll_if_needed($autotable_sw, img, textview)
958 $notebook.set_page(1)
960 perform_color_swap_and_cleanup.call
962 autoscroll_if_needed($autotable_sw, img, textview)
963 $notebook.set_page(1)
968 change_frame_offset_and_cleanup_real = Proc.new { |values|
969 perform_change_frame_offset_and_cleanup = Proc.new { |val|
970 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
971 my_gen_real_thumbnail.call
973 perform_change_frame_offset_and_cleanup.call(values[:new])
975 save_undo(_("specify frame offset"),
977 perform_change_frame_offset_and_cleanup.call(values[:old])
979 autoscroll_if_needed($autotable_sw, img, textview)
980 $notebook.set_page(1)
982 perform_change_frame_offset_and_cleanup.call(values[:new])
984 autoscroll_if_needed($autotable_sw, img, textview)
985 $notebook.set_page(1)
990 change_frame_offset_and_cleanup = Proc.new {
991 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
992 change_frame_offset_and_cleanup_real.call(values)
996 change_pano_amount_and_cleanup_real = Proc.new { |values|
997 perform_change_pano_amount_and_cleanup = Proc.new { |val|
998 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1000 perform_change_pano_amount_and_cleanup.call(values[:new])
1002 save_undo(_("change panorama amount"),
1004 perform_change_pano_amount_and_cleanup.call(values[:old])
1006 autoscroll_if_needed($autotable_sw, img, textview)
1007 $notebook.set_page(1)
1009 perform_change_pano_amount_and_cleanup.call(values[:new])
1011 autoscroll_if_needed($autotable_sw, img, textview)
1012 $notebook.set_page(1)
1017 change_pano_amount_and_cleanup = Proc.new {
1018 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1019 change_pano_amount_and_cleanup_real.call(values)
1023 whitebalance_and_cleanup_real = Proc.new { |values|
1024 perform_change_whitebalance_and_cleanup = Proc.new { |val|
1025 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1026 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1027 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1028 cleanup_all_thumbnails.call
1030 perform_change_whitebalance_and_cleanup.call(values[:new])
1032 save_undo(_("fix white balance"),
1034 perform_change_whitebalance_and_cleanup.call(values[:old])
1036 autoscroll_if_needed($autotable_sw, img, textview)
1037 $notebook.set_page(1)
1039 perform_change_whitebalance_and_cleanup.call(values[:new])
1041 autoscroll_if_needed($autotable_sw, img, textview)
1042 $notebook.set_page(1)
1047 whitebalance_and_cleanup = Proc.new {
1048 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1049 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1050 whitebalance_and_cleanup_real.call(values)
1054 enhance_and_cleanup = Proc.new {
1055 perform_enhance_and_cleanup = Proc.new {
1056 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1057 my_gen_real_thumbnail.call
1060 cleanup_all_thumbnails.call
1061 perform_enhance_and_cleanup.call
1063 save_undo(_("enhance"),
1065 perform_enhance_and_cleanup.call
1067 autoscroll_if_needed($autotable_sw, img, textview)
1068 $notebook.set_page(1)
1070 perform_enhance_and_cleanup.call
1072 autoscroll_if_needed($autotable_sw, img, textview)
1073 $notebook.set_page(1)
1078 delete = Proc.new { |isacut|
1079 if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed?")), { :okcancel => true })
1082 perform_delete = Proc.new {
1083 after = autotable.get_next_widget(vbox)
1085 after = autotable.get_previous_widget(vbox)
1087 if $config['deleteondisk'] && !isacut
1088 msg 3, "scheduling for delete: #{fullpath}"
1089 $todelete << fullpath
1091 autotable.remove(vbox)
1093 $vbox2widgets[after][:textview].grab_focus
1094 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1098 previous_pos = autotable.get_current_number(vbox)
1102 if $xmldir.child_byname_notattr('dir', 'deleted')
1103 $xmldir.delete_attribute('thumbnails-caption')
1104 $xmldir.delete_attribute('thumbnails-captionfile')
1106 $xmldir.add_attribute('deleted', 'true')
1108 while moveup.parent.name == 'dir'
1109 moveup = moveup.parent
1110 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1111 moveup.add_attribute('deleted', 'true')
1117 save_changes('forced')
1118 populate_subalbums_treeview
1120 save_undo(_("delete"),
1122 autotable.reinsert(pos, vbox, filename)
1123 $notebook.set_page(1)
1124 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1126 msg 3, "removing deletion schedule of: #{fullpath}"
1127 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1130 $notebook.set_page(1)
1139 $cuts << { :vbox => vbox, :filename => filename }
1140 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1145 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1148 autotable.queue_draws << proc {
1149 $vbox2widgets[last[:vbox]][:textview].grab_focus
1150 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1152 save_undo(_("paste"),
1154 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1155 $notebook.set_page(1)
1158 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1160 $notebook.set_page(1)
1163 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1168 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1169 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1170 :whitebalance => whitebalance_and_cleanup_real, :pano => change_pano_amount_and_cleanup_real }
1172 textview.signal_connect('key-press-event') { |w, event|
1175 x, y = autotable.get_current_pos(vbox)
1176 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1177 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1178 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1179 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1181 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1182 $vbox2widgets[widget_up][:textview].grab_focus
1189 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1191 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1192 $vbox2widgets[widget_down][:textview].grab_focus
1199 if event.keyval == Gdk::Keyval::GDK_Left
1202 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1209 rotate_and_cleanup.call(-90)
1212 if event.keyval == Gdk::Keyval::GDK_Right
1213 next_ = autotable.get_next_widget(vbox)
1214 if next_ && autotable.get_current_pos(next_)[0] > x
1216 $vbox2widgets[next_][:textview].grab_focus
1223 rotate_and_cleanup.call(90)
1226 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1229 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1230 view_element(filename, { :delete => delete })
1233 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1236 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1240 !propagate #- propagate if needed
1243 $ignore_next_release = false
1244 evtbox.signal_connect('button-press-event') { |w, event|
1245 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1246 if event.state & Gdk::Window::BUTTON3_MASK != 0
1247 #- gesture redo: hold right mouse button then click left mouse button
1248 $config['nogestures'] or perform_redo
1249 $ignore_next_release = true
1251 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1253 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1255 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1256 elsif $enhance.active?
1257 enhance_and_cleanup.call
1258 elsif $delete.active?
1262 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1265 $button1_pressed_autotable = true
1266 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1267 if event.state & Gdk::Window::BUTTON1_MASK != 0
1268 #- gesture undo: hold left mouse button then click right mouse button
1269 $config['nogestures'] or perform_undo
1270 $ignore_next_release = true
1272 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1273 view_element(filename, { :delete => delete })
1278 evtbox.signal_connect('button-release-event') { |w, event|
1279 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1280 if !$ignore_next_release
1281 x, y = autotable.get_current_pos(vbox)
1282 next_ = autotable.get_next_widget(vbox)
1283 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1284 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1285 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1286 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1287 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1288 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1289 :pano => change_pano_amount_and_cleanup })
1291 $ignore_next_release = false
1292 $gesture_press = nil
1297 #- handle reordering with drag and drop
1298 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1299 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1300 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1301 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1304 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1306 #- mouse gesture first (dnd disables button-release-event)
1307 if $gesture_press && $gesture_press[:filename] == filename
1308 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1309 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1310 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1311 rotate_and_cleanup.call(angle)
1312 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1314 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1315 msg 3, "gesture delete: click-drag right button to the bottom"
1317 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1322 ctxt.targets.each { |target|
1323 if target.name == 'reorder-elements'
1324 move_dnd = Proc.new { |from,to|
1327 autotable.move(from, to)
1328 save_undo(_("reorder"),
1329 Proc.new { |from, to|
1331 autotable.move(to - 1, from)
1333 autotable.move(to, from + 1)
1335 $notebook.set_page(1)
1337 autotable.move(from, to)
1338 $notebook.set_page(1)
1343 if $multiple_dnd.size == 0
1344 move_dnd.call(selection_data.data.to_i,
1345 autotable.get_current_number(vbox))
1347 UndoHandler.begin_batch
1348 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1350 #- need to update current position between each call
1351 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1352 autotable.get_current_number(vbox))
1354 UndoHandler.end_batch
1365 def create_auto_table
1367 $autotable = Gtk::AutoTable.new(5)
1369 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1370 thumbnails_vb = Gtk::VBox.new(false, 5)
1372 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1373 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1374 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1375 thumbnails_vb.add($autotable)
1377 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1378 $autotable_sw.add_with_viewport(thumbnails_vb)
1380 #- follows stuff for handling multiple elements selection
1381 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1383 update_selected = Proc.new {
1384 $autotable.current_order.each { |path|
1385 w = $name2widgets[path][:evtbox].window
1386 xm = w.position[0] + w.size[0]/2
1387 ym = w.position[1] + w.size[1]/2
1388 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1389 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1390 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1391 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1394 if $selected_elements[path] && ! $selected_elements[path][:keep]
1395 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))
1396 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1397 $selected_elements.delete(path)
1402 $autotable.signal_connect('realize') { |w,e|
1403 gc = Gdk::GC.new($autotable.window)
1404 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1405 gc.function = Gdk::GC::INVERT
1406 #- autoscroll handling for DND and multiple selections
1407 Gtk.timeout_add(100) {
1408 w, x, y, mask = $autotable.window.pointer
1409 if mask & Gdk::Window::BUTTON1_MASK != 0
1410 if y < $autotable_sw.vadjustment.value
1412 $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]])
1414 if $button1_pressed_autotable || press_x
1415 scroll_upper($autotable_sw, y)
1418 w, pos_x, pos_y = $autotable.window.pointer
1419 $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]])
1420 update_selected.call
1423 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1425 $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]])
1427 if $button1_pressed_autotable || press_x
1428 scroll_lower($autotable_sw, y)
1431 w, pos_x, pos_y = $autotable.window.pointer
1432 $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]])
1433 update_selected.call
1441 $autotable.signal_connect('button-press-event') { |w,e|
1443 if !$button1_pressed_autotable
1446 if e.state & Gdk::Window::SHIFT_MASK == 0
1447 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1448 $selected_elements = {}
1449 $statusbar.push(0, utf8(_("Nothing selected.")))
1451 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1453 set_mousecursor(Gdk::Cursor::TCROSS)
1457 $autotable.signal_connect('button-release-event') { |w,e|
1459 if $button1_pressed_autotable
1460 #- unselect all only now
1461 $multiple_dnd = $selected_elements.keys
1462 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1463 $selected_elements = {}
1464 $button1_pressed_autotable = false
1467 $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]])
1468 if $selected_elements.length > 0
1469 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1472 press_x = press_y = pos_x = pos_y = nil
1473 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1477 $autotable.signal_connect('motion-notify-event') { |w,e|
1480 $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]])
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]])
1485 update_selected.call
1491 def create_subalbums_page
1493 subalbums_hb = Gtk::HBox.new
1494 $subalbums_vb = Gtk::VBox.new(false, 5)
1495 subalbums_hb.pack_start($subalbums_vb, false, false)
1496 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1497 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1498 $subalbums_sw.add_with_viewport(subalbums_hb)
1501 def save_current_file
1505 ios = File.open($filename, "w")
1506 $xmldoc.write(ios, 0)
1511 def save_current_file_user
1512 save_tempfilename = $filename
1513 $filename = $orig_filename
1516 $generated_outofline = false
1517 $filename = save_tempfilename
1519 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1520 $todelete.each { |f|
1521 system("rm -f #{f}")
1525 def mark_document_as_dirty
1526 $xmldoc.elements.each('//dir') { |elem|
1527 elem.delete_attribute('already-generated')
1531 #- ret: true => ok false => cancel
1532 def ask_save_modifications(msg1, msg2, *options)
1534 options = options.size > 0 ? options[0] : {}
1536 if options[:disallow_cancel]
1537 dialog = Gtk::Dialog.new(msg1,
1539 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1540 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1541 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1543 dialog = Gtk::Dialog.new(msg1,
1545 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1546 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1547 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1548 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1550 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1551 dialog.vbox.add(Gtk::Label.new(msg2))
1552 dialog.window_position = Gtk::Window::POS_CENTER
1555 dialog.run { |response|
1557 if response == Gtk::Dialog::RESPONSE_YES
1558 save_current_file_user
1560 #- if we have generated an album but won't save modifications, we must remove
1561 #- already-generated markers in original file
1562 if $generated_outofline
1564 $xmldoc = REXML::Document.new File.new($orig_filename)
1565 mark_document_as_dirty
1566 ios = File.open($orig_filename, "w")
1567 $xmldoc.write(ios, 0)
1570 puts "exception: #{$!}"
1574 if response == Gtk::Dialog::RESPONSE_CANCEL
1577 $todelete = [] #- unconditionally clear the list of images/videos to delete
1583 def try_quit(*options)
1584 if ask_save_modifications(utf8(_("Save before quitting?")),
1585 utf8(_("Do you want to save your changes before quitting?")),
1591 def show_popup(parent, msg, *options)
1592 dialog = Gtk::Dialog.new
1593 if options[0] && options[0][:title]
1594 dialog.title = options[0][:title]
1596 dialog.title = utf8(_("Booh message"))
1598 lbl = Gtk::Label.new
1599 if options[0] && options[0][:nomarkup]
1604 if options[0] && options[0][:centered]
1605 lbl.set_justify(Gtk::Justification::CENTER)
1607 if options[0] && options[0][:selectable]
1608 lbl.selectable = true
1610 if options[0] && options[0][:topwidget]
1611 dialog.vbox.add(options[0][:topwidget])
1613 if options[0] && options[0][:scrolled]
1614 sw = Gtk::ScrolledWindow.new(nil, nil)
1615 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1616 sw.add_with_viewport(lbl)
1618 dialog.set_default_size(400, 500)
1620 dialog.vbox.add(lbl)
1621 dialog.set_default_size(200, 120)
1623 if options[0] && options[0][:okcancel]
1624 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1626 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1628 if options[0] && options[0][:pos_centered]
1629 dialog.window_position = Gtk::Window::POS_CENTER
1631 dialog.window_position = Gtk::Window::POS_MOUSE
1634 if options[0] && options[0][:linkurl]
1635 linkbut = Gtk::Button.new('')
1636 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1637 linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1638 linkbut.relief = Gtk::RELIEF_NONE
1639 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1640 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1641 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1646 if !options[0] || !options[0][:not_transient]
1647 dialog.transient_for = parent
1648 dialog.run { |response|
1650 if options[0] && options[0][:okcancel]
1651 return response == Gtk::Dialog::RESPONSE_OK
1655 dialog.signal_connect('response') { dialog.destroy }
1659 def backend_wait_message(parent, msg, infopipe_path, mode)
1661 w.set_transient_for(parent)
1664 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1665 vb.pack_start(Gtk::Label.new(msg), false, false)
1667 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1668 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1669 if mode != 'one dir scan'
1670 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1672 if mode == 'web-album'
1673 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1674 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1676 vb.pack_start(Gtk::HSeparator.new, false, false)
1678 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1679 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1680 vb.pack_end(bottom, false, false)
1682 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1683 refresh_thread = Thread.new {
1684 directories_counter = 0
1685 while line = infopipe.gets
1686 if line =~ /^directories: (\d+), sizes: (\d+)/
1687 directories = $1.to_f + 1
1689 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1690 elements = $3.to_f + 1
1691 if mode == 'web-album'
1695 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = 0 }
1696 if mode != 'one dir scan'
1697 newtext = utf8(full_src_dir_to_rel($1, $2))
1698 newtext = '/' if newtext == ''
1699 gtk_thread_protect { puts "destroyed: " + pb1_2.destroyed?.to_s; pb1_2.text = newtext }
1700 directories_counter += 1
1701 gtk_thread_protect { puts "destroyed: " + pb1_2.destroyed?.to_s; pb1_2.fraction = directories_counter / directories }
1703 elsif line =~ /^processing element$/
1704 element_counter += 1
1705 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = element_counter / elements }
1706 elsif line =~ /^processing size$/
1707 element_counter += 1
1708 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = element_counter / elements }
1709 elsif line =~ /^finished processing sizes$/
1710 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = 1 }
1711 elsif line =~ /^creating index.html$/
1712 gtk_thread_protect { puts "destroyed: " + pb1_2.destroyed?.to_s; pb1_2.text = utf8(_("finished")) }
1713 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = pb1_2.fraction = 1 }
1714 directories_counter = 0
1715 elsif line =~ /^index.html: (.+)\|(.+)/
1716 newtext = utf8(full_src_dir_to_rel($1, $2))
1717 newtext = '/' if newtext == ''
1718 gtk_thread_protect { puts "destroyed: " + pb2.destroyed?.to_s; pb2.text = newtext }
1719 directories_counter += 1
1720 gtk_thread_protect { puts "destroyed: " + pb2.destroyed?.to_s; pb2.fraction = directories_counter / directories }
1721 elsif line =~ /^die: (.*)$/
1728 w.signal_connect('delete-event') { w.destroy }
1729 w.signal_connect('destroy') {
1730 Thread.kill(refresh_thread)
1731 gtk_thread_abandon #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1734 system("rm -f #{infopipe_path}")
1737 w.window_position = Gtk::Window::POS_CENTER
1743 def call_backend(cmd, waitmsg, mode, params)
1744 pipe = Tempfile.new("boohpipe")
1746 system("mkfifo #{pipe.path}")
1747 cmd += " --info-pipe #{pipe.path}"
1748 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
1753 id, exitstatus = Process.waitpid2(pid)
1754 gtk_thread_protect { puts "destroyed: " + w8.destroyed?.to_s; w8.destroy }
1756 if params[:successmsg]
1757 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
1759 if params[:closure_after]
1760 gtk_thread_protect(¶ms[:closure_after])
1762 elsif exitstatus == 15
1763 #- say nothing, user aborted
1765 gtk_thread_protect { show_popup($main_window,
1766 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
1772 button.signal_connect('clicked') {
1773 Process.kill('SIGTERM', pid)
1777 def save_changes(*forced)
1778 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1782 $xmldir.delete_attribute('already-generated')
1784 propagate_children = Proc.new { |xmldir|
1785 if xmldir.attributes['subdirs-caption']
1786 xmldir.delete_attribute('already-generated')
1788 xmldir.elements.each('dir') { |element|
1789 propagate_children.call(element)
1793 if $xmldir.child_byname_notattr('dir', 'deleted')
1794 new_title = $subalbums_title.buffer.text
1795 if new_title != $xmldir.attributes['subdirs-caption']
1796 parent = $xmldir.parent
1797 if parent.name == 'dir'
1798 parent.delete_attribute('already-generated')
1800 propagate_children.call($xmldir)
1802 $xmldir.add_attribute('subdirs-caption', new_title)
1803 $xmldir.elements.each('dir') { |element|
1804 if !element.attributes['deleted']
1805 path = element.attributes['path']
1806 newtext = $subalbums_edits[path][:editzone].buffer.text
1807 if element.attributes['subdirs-caption']
1808 if element.attributes['subdirs-caption'] != newtext
1809 propagate_children.call(element)
1811 element.add_attribute('subdirs-caption', newtext)
1812 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1814 if element.attributes['thumbnails-caption'] != newtext
1815 element.delete_attribute('already-generated')
1817 element.add_attribute('thumbnails-caption', newtext)
1818 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1824 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
1825 if $xmldir.attributes['thumbnails-caption']
1826 path = $xmldir.attributes['path']
1827 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1829 elsif $xmldir.attributes['thumbnails-caption']
1830 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
1833 #- remove and reinsert elements to reflect new ordering
1836 $xmldir.elements.each { |element|
1837 if element.name == 'image' || element.name == 'video'
1838 saves[element.attributes['filename']] = element.remove
1842 $autotable.current_order.each { |path|
1843 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1844 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1847 saves.each_key { |path|
1848 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1849 chld.add_attribute('deleted', 'true')
1853 def remove_all_captions
1856 $autotable.current_order.each { |path|
1857 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
1858 $name2widgets[File.basename(path)][:textview].buffer.text = ''
1860 save_undo(_("remove all captions"),
1862 texts.each_key { |key|
1863 $name2widgets[key][:textview].buffer.text = texts[key]
1865 $notebook.set_page(1)
1867 texts.each_key { |key|
1868 $name2widgets[key][:textview].buffer.text = ''
1870 $notebook.set_page(1)
1876 $selected_elements.each_key { |path|
1877 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1883 $selected_elements = {}
1887 $undo_tb.sensitive = $undo_mb.sensitive = false
1888 $redo_tb.sensitive = $redo_mb.sensitive = false
1894 $subalbums_vb.children.each { |chld|
1895 $subalbums_vb.remove(chld)
1897 $subalbums = Gtk::Table.new(0, 0, true)
1898 current_y_sub_albums = 0
1900 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
1901 $subalbums_edits = {}
1902 subalbums_counter = 0
1903 subalbums_edits_bypos = {}
1905 add_subalbum = Proc.new { |xmldir, counter|
1906 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
1907 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
1908 if xmldir == $xmldir
1909 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
1910 caption = xmldir.attributes['thumbnails-caption']
1911 captionfile, dummy = find_subalbum_caption_info(xmldir)
1912 infotype = 'thumbnails'
1914 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
1915 captionfile, caption = find_subalbum_caption_info(xmldir)
1916 infotype = find_subalbum_info_type(xmldir)
1918 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
1919 hbox = Gtk::HBox.new
1920 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
1922 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
1925 my_gen_real_thumbnail = proc {
1926 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
1929 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
1930 f.add(img = Gtk::Image.new)
1931 my_gen_real_thumbnail.call
1933 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
1935 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
1936 $subalbums.attach(hbox,
1937 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1939 frame, textview = create_editzone($subalbums_sw, 0, img)
1940 textview.buffer.text = caption
1941 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
1942 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1944 change_image = Proc.new {
1945 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
1947 Gtk::FileChooser::ACTION_OPEN,
1949 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1950 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
1951 fc.transient_for = $main_window
1952 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))
1953 f.add(preview_img = Gtk::Image.new)
1955 fc.signal_connect('update-preview') { |w|
1957 if fc.preview_filename
1958 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
1959 fc.preview_widget_active = true
1961 rescue Gdk::PixbufError
1962 fc.preview_widget_active = false
1965 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1967 old_file = captionfile
1968 old_rotate = xmldir.attributes["#{infotype}-rotate"]
1969 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
1970 old_enhance = xmldir.attributes["#{infotype}-enhance"]
1971 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
1973 new_file = fc.filename
1974 msg 3, "new captionfile is: #{fc.filename}"
1975 perform_changefile = Proc.new {
1976 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
1977 $modified_pixbufs.delete(thumbnail_file)
1978 xmldir.delete_attribute("#{infotype}-rotate")
1979 xmldir.delete_attribute("#{infotype}-color-swap")
1980 xmldir.delete_attribute("#{infotype}-enhance")
1981 xmldir.delete_attribute("#{infotype}-frame-offset")
1982 my_gen_real_thumbnail.call
1984 perform_changefile.call
1986 save_undo(_("change caption file for sub-album"),
1988 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
1989 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
1990 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
1991 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
1992 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
1993 my_gen_real_thumbnail.call
1994 $notebook.set_page(0)
1996 perform_changefile.call
1997 $notebook.set_page(0)
2004 rotate_and_cleanup = Proc.new { |angle|
2005 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2006 system("rm -f '#{thumbnail_file}'")
2009 move = Proc.new { |direction|
2012 save_changes('forced')
2013 if direction == 'up'
2014 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2015 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2016 subalbums_edits_bypos[oldpos - 1][:position] += 1
2018 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2019 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2020 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2024 $xmldir.elements.each('dir') { |element|
2025 if (!element.attributes['deleted'])
2026 elems << [ element.attributes['path'], element.remove ]
2029 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2030 each { |e| $xmldir.add_element(e[1]) }
2031 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2032 $xmldir.elements.each('descendant::dir') { |elem|
2033 elem.delete_attribute('already-generated')
2038 color_swap_and_cleanup = Proc.new {
2039 perform_color_swap_and_cleanup = Proc.new {
2040 color_swap(xmldir, "#{infotype}-")
2041 my_gen_real_thumbnail.call
2043 perform_color_swap_and_cleanup.call
2045 save_undo(_("color swap"),
2047 perform_color_swap_and_cleanup.call
2048 $notebook.set_page(0)
2050 perform_color_swap_and_cleanup.call
2051 $notebook.set_page(0)
2056 change_frame_offset_and_cleanup = Proc.new {
2057 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2058 perform_change_frame_offset_and_cleanup = Proc.new { |val|
2059 change_frame_offset(xmldir, "#{infotype}-", val)
2060 my_gen_real_thumbnail.call
2062 perform_change_frame_offset_and_cleanup.call(values[:new])
2064 save_undo(_("specify frame offset"),
2066 perform_change_frame_offset_and_cleanup.call(values[:old])
2067 $notebook.set_page(0)
2069 perform_change_frame_offset_and_cleanup.call(values[:new])
2070 $notebook.set_page(0)
2076 whitebalance_and_cleanup = Proc.new {
2077 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2078 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2079 perform_change_whitebalance_and_cleanup = Proc.new { |val|
2080 change_whitebalance(xmldir, "#{infotype}-", val)
2081 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2082 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2083 system("rm -f '#{thumbnail_file}'")
2085 perform_change_whitebalance_and_cleanup.call(values[:new])
2087 save_undo(_("fix white balance"),
2089 perform_change_whitebalance_and_cleanup.call(values[:old])
2090 $notebook.set_page(0)
2092 perform_change_whitebalance_and_cleanup.call(values[:new])
2093 $notebook.set_page(0)
2099 enhance_and_cleanup = Proc.new {
2100 perform_enhance_and_cleanup = Proc.new {
2101 enhance(xmldir, "#{infotype}-")
2102 my_gen_real_thumbnail.call
2105 perform_enhance_and_cleanup.call
2107 save_undo(_("enhance"),
2109 perform_enhance_and_cleanup.call
2110 $notebook.set_page(0)
2112 perform_enhance_and_cleanup.call
2113 $notebook.set_page(0)
2118 evtbox.signal_connect('button-press-event') { |w, event|
2119 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2121 rotate_and_cleanup.call(90)
2123 rotate_and_cleanup.call(-90)
2124 elsif $enhance.active?
2125 enhance_and_cleanup.call
2128 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2129 popup_thumbnail_menu(event, ['change_image'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2130 { :forbid_left => true, :forbid_right => true,
2131 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter },
2132 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2133 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
2135 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2140 evtbox.signal_connect('button-press-event') { |w, event|
2141 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2145 evtbox.signal_connect('button-release-event') { |w, event|
2146 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2147 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2148 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2149 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2150 msg 3, "gesture rotate: #{angle}"
2151 rotate_and_cleanup.call(angle)
2154 $gesture_press = nil
2157 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2158 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2159 current_y_sub_albums += 1
2162 if $xmldir.child_byname_notattr('dir', 'deleted')
2164 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2165 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2166 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2167 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2168 #- this album image/caption
2169 if $xmldir.attributes['thumbnails-caption']
2170 add_subalbum.call($xmldir, 0)
2173 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2174 $xmldir.elements.each { |element|
2175 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2176 #- element (image or video) of this album
2177 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2178 msg 3, "dest_img: #{dest_img}"
2179 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
2180 total[element.name] += 1
2182 if element.name == 'dir' && !element.attributes['deleted']
2183 #- sub-album image/caption
2184 add_subalbum.call(element, subalbums_counter += 1)
2185 total[element.name] += 1
2188 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2189 total['image'], total['video'], total['dir'] ]))
2190 $subalbums_vb.add($subalbums)
2191 $subalbums_vb.show_all
2193 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2194 $notebook.get_tab_label($autotable_sw).sensitive = false
2195 $notebook.set_page(0)
2196 $thumbnails_title.buffer.text = ''
2198 $notebook.get_tab_label($autotable_sw).sensitive = true
2199 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2202 if !$xmldir.child_byname_notattr('dir', 'deleted')
2203 $notebook.get_tab_label($subalbums_sw).sensitive = false
2204 $notebook.set_page(1)
2206 $notebook.get_tab_label($subalbums_sw).sensitive = true
2210 def pixbuf_or_nil(filename)
2212 return Gdk::Pixbuf.new(filename)
2218 def theme_choose(current)
2219 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2221 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2222 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2223 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2225 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2226 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2227 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2228 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2229 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2230 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2231 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2232 treeview.signal_connect('button-press-event') { |w, event|
2233 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2234 dialog.response(Gtk::Dialog::RESPONSE_OK)
2238 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2240 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2243 iter[0] = File.basename(dir)
2244 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2245 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2246 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2247 if File.basename(dir) == current
2248 treeview.selection.select_iter(iter)
2252 dialog.set_default_size(700, 400)
2253 dialog.vbox.show_all
2254 dialog.run { |response|
2255 iter = treeview.selection.selected
2257 if response == Gtk::Dialog::RESPONSE_OK && iter
2258 return model.get_value(iter, 0)
2264 def show_password_protections
2265 examine_dir_elem = Proc.new { |parent_iter, xmldir, already_protected|
2266 child_iter = $albums_iters[xmldir.attributes['path']]
2267 if xmldir.attributes['password-protect']
2268 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2269 already_protected = true
2270 elsif already_protected
2271 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2273 pix = pix.saturate_and_pixelate(1, true)
2279 xmldir.elements.each('dir') { |elem|
2280 if !elem.attributes['deleted']
2281 examine_dir_elem.call(child_iter, elem, already_protected)
2285 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2288 def populate_subalbums_treeview
2292 $subalbums_vb.children.each { |chld|
2293 $subalbums_vb.remove(chld)
2296 source = $xmldoc.root.attributes['source']
2297 msg 3, "source: #{source}"
2299 xmldir = $xmldoc.elements['//dir']
2300 if !xmldir || xmldir.attributes['path'] != source
2301 msg 1, _("Corrupted booh file...")
2305 append_dir_elem = Proc.new { |parent_iter, xmldir|
2306 child_iter = $albums_ts.append(parent_iter)
2307 child_iter[0] = File.basename(xmldir.attributes['path'])
2308 child_iter[1] = xmldir.attributes['path']
2309 $albums_iters[xmldir.attributes['path']] = child_iter
2310 msg 3, "puttin location: #{xmldir.attributes['path']}"
2311 xmldir.elements.each('dir') { |elem|
2312 if !elem.attributes['deleted']
2313 append_dir_elem.call(child_iter, elem)
2317 append_dir_elem.call(nil, xmldir)
2318 show_password_protections
2320 $albums_tv.expand_all
2321 $albums_tv.selection.select_iter($albums_ts.iter_first)
2324 def open_file(filename)
2328 $current_path = nil #- invalidate
2329 $modified_pixbufs = {}
2332 $subalbums_vb.children.each { |chld|
2333 $subalbums_vb.remove(chld)
2336 if !File.exists?(filename)
2337 return utf8(_("File not found."))
2341 $xmldoc = REXML::Document.new File.new(filename)
2346 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2347 if entry2type(filename).nil?
2348 return utf8(_("Not a booh file!"))
2350 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."))
2354 if !source = $xmldoc.root.attributes['source']
2355 return utf8(_("Corrupted booh file..."))
2358 if !dest = $xmldoc.root.attributes['destination']
2359 return utf8(_("Corrupted booh file..."))
2362 if !theme = $xmldoc.root.attributes['theme']
2363 return utf8(_("Corrupted booh file..."))
2366 if $xmldoc.root.attributes['version'] != $VERSION
2367 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2368 mark_document_as_dirty
2369 if $xmldoc.root.attributes['version'] < '0.8.4'
2370 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2371 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2372 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2373 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2374 if old_dest_dir != new_dest_dir
2375 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2377 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2378 xmldir.elements.each { |element|
2379 if %w(image video).include?(element.name) && !element.attributes['deleted']
2380 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2381 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2382 Dir[old_name + '*'].each { |file|
2383 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2384 file != new_file and sys("mv '#{file}' '#{new_file}'")
2387 if element.name == 'dir' && !element.attributes['deleted']
2388 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2389 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2390 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2394 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2398 $xmldoc.root.add_attribute('version', $VERSION)
2401 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2402 optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2403 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2405 $filename = filename
2406 select_theme(theme, limit_sizes, optimizefor32, nperrow)
2407 $default_size['thumbnails'] =~ /(.*)x(.*)/
2408 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2409 $albums_thumbnail_size =~ /(.*)x(.*)/
2410 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2412 populate_subalbums_treeview
2414 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = true
2418 def open_file_user(filename)
2419 result = open_file(filename)
2421 $config['last-opens'] ||= []
2422 if $config['last-opens'][-1] != utf8(filename)
2423 $config['last-opens'] << utf8(filename)
2425 $orig_filename = $filename
2426 tmp = Tempfile.new("boohtemp")
2429 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2431 $tempfiles << $filename << "#{$filename}.backup"
2433 $orig_filename = nil
2439 if !ask_save_modifications(utf8(_("Save this album?")),
2440 utf8(_("Do you want to save the changes to this album?")),
2441 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2444 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2446 Gtk::FileChooser::ACTION_OPEN,
2448 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2449 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2450 fc.set_current_folder(File.expand_path("~/.booh"))
2451 fc.transient_for = $main_window
2454 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2455 push_mousecursor_wait(fc)
2456 msg = open_file_user(fc.filename)
2472 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2477 def additional_booh_options
2480 options += "--mproc #{$config['mproc'].to_i} "
2482 if $config['emptycomments']
2483 options += "--empty-comments "
2489 if !ask_save_modifications(utf8(_("Save this album?")),
2490 utf8(_("Do you want to save the changes to this album?")),
2491 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2494 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2496 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2497 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2498 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2500 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2501 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2502 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2503 tbl.attach(src = Gtk::Entry.new,
2504 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2505 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2506 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2507 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2508 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2509 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2510 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2511 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2512 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2513 tbl.attach(dest = Gtk::Entry.new,
2514 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2515 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2516 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2517 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2518 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2519 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2520 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2521 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2522 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2524 tooltips = Gtk::Tooltips.new
2525 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2526 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2527 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2528 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2529 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2530 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2531 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)
2532 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2533 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2534 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2535 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2536 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)
2538 src_nb_calculated_for = ''
2540 process_src_nb = Proc.new {
2541 if src.text != src_nb_calculated_for
2542 src_nb_calculated_for = src.text
2544 Thread.kill(src_nb_thread)
2547 if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2548 if File.readable?(from_utf8(src_nb_calculated_for))
2549 src_nb_thread = Thread.new {
2550 gtk_thread_protect { puts "destroyed: " + src_nb.destroyed?.to_s; src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2551 total = { 'image' => 0, 'video' => 0, nil => 0 }
2552 `find '#{from_utf8(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2553 if File.basename(dir) =~ /^\./
2557 Dir.entries(dir.chomp).each { |file|
2558 total[entry2type(file)] += 1
2560 rescue Errno::EACCES, Errno::ENOENT
2564 gtk_thread_protect { puts "destroyed: " + src_nb.destroyed?.to_s; src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2568 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2571 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2576 timeout_src_nb = Gtk.timeout_add(100) {
2580 src_browse.signal_connect('clicked') {
2581 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2583 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2585 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2586 fc.transient_for = $main_window
2587 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2588 src.text = utf8(fc.filename)
2590 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2595 dest_browse.signal_connect('clicked') {
2596 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2598 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2600 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2601 fc.transient_for = $main_window
2602 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2603 dest.text = utf8(fc.filename)
2608 conf_browse.signal_connect('clicked') {
2609 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2611 Gtk::FileChooser::ACTION_SAVE,
2613 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2614 fc.transient_for = $main_window
2615 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2616 fc.set_current_folder(File.expand_path("~/.booh"))
2617 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2618 conf.text = utf8(fc.filename)
2625 recreate_theme_config = proc {
2626 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2628 select_theme(theme_button.label, 'all', optimize432.active?, nil)
2629 $images_size.each { |s|
2630 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2634 tooltips.set_tip(cb, utf8(s['description']), nil)
2635 theme_sizes << { :widget => cb, :value => s['name'] }
2637 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2638 tooltips = Gtk::Tooltips.new
2639 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2640 theme_sizes << { :widget => cb, :value => 'original' }
2643 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2646 $allowed_N_values.each { |n|
2648 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2650 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2652 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2656 nperrows << { :widget => rb, :value => n }
2658 nperrowradios.show_all
2660 recreate_theme_config.call
2662 theme_button.signal_connect('clicked') {
2663 if newtheme = theme_choose(theme_button.label)
2664 theme_button.label = newtheme
2665 recreate_theme_config.call
2669 dialog.vbox.add(frame1)
2670 dialog.vbox.add(frame2)
2671 dialog.window_position = Gtk::Window::POS_MOUSE
2677 dialog.run { |response|
2678 if response == Gtk::Dialog::RESPONSE_OK
2679 srcdir = from_utf8(src.text)
2680 destdir = from_utf8(dest.text)
2681 if !File.directory?(srcdir)
2682 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2684 elsif conf.text == ''
2685 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2687 elsif File.directory?(from_utf8(conf.text))
2688 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
2690 elsif destdir != make_dest_filename(destdir)
2691 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2693 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
2694 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2696 elsif File.exists?(destdir) && !File.directory?(destdir)
2697 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2699 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2700 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2702 system("mkdir '#{destdir}'")
2703 if !File.directory?(destdir)
2704 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2715 srcdir = from_utf8(src.text)
2716 destdir = from_utf8(dest.text)
2717 configskel = File.expand_path(from_utf8(conf.text))
2718 theme = theme_button.label
2719 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2720 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2721 opt432 = optimize432.active?
2722 madewith = madewithentry.text
2724 Thread.kill(src_nb_thread)
2725 gtk_thread_abandon #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2728 Gtk.timeout_remove(timeout_src_nb)
2731 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2732 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2733 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' #{additional_booh_options}",
2734 utf8(_("Please wait while scanning source directory...")),
2736 { :closure_after => proc { open_file_user(configskel) } })
2741 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2743 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2744 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2745 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2747 source = $xmldoc.root.attributes['source']
2748 dest = $xmldoc.root.attributes['destination']
2749 theme = $xmldoc.root.attributes['theme']
2750 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2751 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2752 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2754 limit_sizes = limit_sizes.split(/,/)
2756 madewith = $xmldoc.root.attributes['made-with']
2758 tooltips = Gtk::Tooltips.new
2759 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2760 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2761 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2762 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2763 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2764 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2765 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2766 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
2767 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2768 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2769 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2770 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
2771 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2773 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2774 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2775 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2776 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2777 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2778 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2779 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)
2780 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2781 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2782 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2783 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2785 madewithentry.text = madewith
2787 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)
2791 recreate_theme_config = proc {
2792 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2794 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
2796 $images_size.each { |s|
2797 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2799 if limit_sizes.include?(s['name'])
2807 tooltips.set_tip(cb, utf8(s['description']), nil)
2808 theme_sizes << { :widget => cb, :value => s['name'] }
2810 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2811 tooltips = Gtk::Tooltips.new
2812 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2813 if limit_sizes && limit_sizes.include?('original')
2816 theme_sizes << { :widget => cb, :value => 'original' }
2819 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2822 $allowed_N_values.each { |n|
2824 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2826 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2828 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2829 nperrowradios.add(Gtk::Label.new(' '))
2830 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
2833 nperrows << { :widget => rb, :value => n.to_s }
2835 nperrowradios.show_all
2837 recreate_theme_config.call
2839 theme_button.signal_connect('clicked') {
2840 if newtheme = theme_choose(theme_button.label)
2843 theme_button.label = newtheme
2844 recreate_theme_config.call
2848 dialog.vbox.add(frame1)
2849 dialog.vbox.add(frame2)
2850 dialog.window_position = Gtk::Window::POS_MOUSE
2856 dialog.run { |response|
2857 if response == Gtk::Dialog::RESPONSE_OK
2858 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2859 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2868 save_theme = theme_button.label
2869 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
2870 save_opt432 = optimize432.active?
2871 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2872 save_madewith = madewithentry.text
2875 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith)
2876 mark_document_as_dirty
2878 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
2879 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
2880 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' #{additional_booh_options}",
2881 utf8(_("Please wait while scanning source directory...")),
2883 { :closure_after => proc {
2884 open_file($filename)
2893 sel = $albums_tv.selection.selected_rows
2895 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2896 "--verbose-level #{$verbose_level} #{additional_booh_options}",
2897 utf8(_("Please wait while scanning source directory...")),
2899 { :closure_after => proc {
2900 open_file($filename)
2901 $albums_tv.selection.select_path(sel[0])
2909 sel = $albums_tv.selection.selected_rows
2911 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2912 "--verbose-level #{$verbose_level} #{additional_booh_options}",
2913 utf8(_("Please wait while scanning source directory...")),
2915 { :closure_after => proc {
2916 open_file($filename)
2917 $albums_tv.selection.select_path(sel[0])
2925 theme = $xmldoc.root.attributes['theme']
2926 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2928 limit_sizes = "--sizes #{limit_sizes}"
2930 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
2931 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
2932 utf8(_("Please wait while scanning source directory...")),
2934 { :closure_after => proc {
2935 open_file($filename)
2941 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
2943 Gtk::FileChooser::ACTION_SAVE,
2945 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2946 fc.transient_for = $main_window
2947 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2948 fc.set_current_folder(File.expand_path("~/.booh"))
2949 fc.filename = $orig_filename
2950 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2951 $orig_filename = fc.filename
2952 save_current_file_user
2958 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
2960 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2961 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2962 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2964 dialog.vbox.add(notebook = Gtk::Notebook.new)
2965 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
2966 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
2967 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2968 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
2969 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2970 tooltips = Gtk::Tooltips.new
2971 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
2972 for example: /usr/bin/mplayer %f")), nil)
2973 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
2974 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2975 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
2976 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2977 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
2978 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
2979 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
2980 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2981 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)),
2982 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2983 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)
2984 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
2985 0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2986 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)
2987 tbl.attach(emptycomments_check = Gtk::CheckButton.new(utf8(_("Use empty comments for new albums"))),
2988 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
2989 tooltips.set_tip(emptycomments_check, utf8(_("Normally, filenames are used as comments for new albums. Check this if you prefer empty comments.")), nil)
2990 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
2991 0, 2, 5, 6, Gtk::FILL, Gtk::SHRINK, 2, 2)
2992 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)
2993 smp_check.signal_connect('toggled') {
2994 if smp_check.active?
2995 smp_hbox.sensitive = true
2997 smp_hbox.sensitive = false
3001 smp_check.active = true
3002 smp_spin.value = $config['mproc'].to_i
3004 nogestures_check.active = $config['nogestures']
3005 emptycomments_check.active = $config['emptycomments']
3006 deleteondisk_check.active = $config['deleteondisk']
3008 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3009 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3010 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3011 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3012 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3014 dialog.vbox.show_all
3015 dialog.run { |response|
3016 if response == Gtk::Dialog::RESPONSE_OK
3017 $config['video-viewer'] = video_viewer_entry.text
3018 $config['browser'] = browser_entry.text
3019 if smp_check.active?
3020 $config['mproc'] = smp_spin.value.to_i
3022 $config.delete('mproc')
3024 $config['nogestures'] = nogestures_check.active?
3025 $config['emptycomments'] = emptycomments_check.active?
3026 $config['deleteondisk'] = deleteondisk_check.active?
3028 $config['convert-enhance'] = enhance_entry.text
3035 if $undo_tb.sensitive?
3036 $redo_tb.sensitive = $redo_mb.sensitive = true
3037 if not more_undoes = UndoHandler.undo($statusbar)
3038 $undo_tb.sensitive = $undo_mb.sensitive = false
3044 if $redo_tb.sensitive?
3045 $undo_tb.sensitive = $undo_mb.sensitive = true
3046 if not more_redoes = UndoHandler.redo($statusbar)
3047 $redo_tb.sensitive = $redo_mb.sensitive = false
3052 def show_one_click_explanation(intro)
3053 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
3055 %s When such a tool is activated
3056 (<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
3057 on a thumbnail will immediately apply the desired action.
3059 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
3065 GNU GENERAL PUBLIC LICENSE
3066 Version 2, June 1991
3068 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
3069 675 Mass Ave, Cambridge, MA 02139, USA
3070 Everyone is permitted to copy and distribute verbatim copies
3071 of this license document, but changing it is not allowed.
3075 The licenses for most software are designed to take away your
3076 freedom to share and change it. By contrast, the GNU General Public
3077 License is intended to guarantee your freedom to share and change free
3078 software--to make sure the software is free for all its users. This
3079 General Public License applies to most of the Free Software
3080 Foundation's software and to any other program whose authors commit to
3081 using it. (Some other Free Software Foundation software is covered by
3082 the GNU Library General Public License instead.) You can apply it to
3085 When we speak of free software, we are referring to freedom, not
3086 price. Our General Public Licenses are designed to make sure that you
3087 have the freedom to distribute copies of free software (and charge for
3088 this service if you wish), that you receive source code or can get it
3089 if you want it, that you can change the software or use pieces of it
3090 in new free programs; and that you know you can do these things.
3092 To protect your rights, we need to make restrictions that forbid
3093 anyone to deny you these rights or to ask you to surrender the rights.
3094 These restrictions translate to certain responsibilities for you if you
3095 distribute copies of the software, or if you modify it.
3097 For example, if you distribute copies of such a program, whether
3098 gratis or for a fee, you must give the recipients all the rights that
3099 you have. You must make sure that they, too, receive or can get the
3100 source code. And you must show them these terms so they know their
3103 We protect your rights with two steps: (1) copyright the software, and
3104 (2) offer you this license which gives you legal permission to copy,
3105 distribute and/or modify the software.
3107 Also, for each author's protection and ours, we want to make certain
3108 that everyone understands that there is no warranty for this free
3109 software. If the software is modified by someone else and passed on, we
3110 want its recipients to know that what they have is not the original, so
3111 that any problems introduced by others will not reflect on the original
3112 authors' reputations.
3114 Finally, any free program is threatened constantly by software
3115 patents. We wish to avoid the danger that redistributors of a free
3116 program will individually obtain patent licenses, in effect making the
3117 program proprietary. To prevent this, we have made it clear that any
3118 patent must be licensed for everyone's free use or not licensed at all.
3120 The precise terms and conditions for copying, distribution and
3121 modification follow.
3124 GNU GENERAL PUBLIC LICENSE
3125 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
3127 0. This License applies to any program or other work which contains
3128 a notice placed by the copyright holder saying it may be distributed
3129 under the terms of this General Public License. The "Program", below,
3130 refers to any such program or work, and a "work based on the Program"
3131 means either the Program or any derivative work under copyright law:
3132 that is to say, a work containing the Program or a portion of it,
3133 either verbatim or with modifications and/or translated into another
3134 language. (Hereinafter, translation is included without limitation in
3135 the term "modification".) Each licensee is addressed as "you".
3137 Activities other than copying, distribution and modification are not
3138 covered by this License; they are outside its scope. The act of
3139 running the Program is not restricted, and the output from the Program
3140 is covered only if its contents constitute a work based on the
3141 Program (independent of having been made by running the Program).
3142 Whether that is true depends on what the Program does.
3144 1. You may copy and distribute verbatim copies of the Program's
3145 source code as you receive it, in any medium, provided that you
3146 conspicuously and appropriately publish on each copy an appropriate
3147 copyright notice and disclaimer of warranty; keep intact all the
3148 notices that refer to this License and to the absence of any warranty;
3149 and give any other recipients of the Program a copy of this License
3150 along with the Program.
3152 You may charge a fee for the physical act of transferring a copy, and
3153 you may at your option offer warranty protection in exchange for a fee.
3155 2. You may modify your copy or copies of the Program or any portion
3156 of it, thus forming a work based on the Program, and copy and
3157 distribute such modifications or work under the terms of Section 1
3158 above, provided that you also meet all of these conditions:
3160 a) You must cause the modified files to carry prominent notices
3161 stating that you changed the files and the date of any change.
3163 b) You must cause any work that you distribute or publish, that in
3164 whole or in part contains or is derived from the Program or any
3165 part thereof, to be licensed as a whole at no charge to all third
3166 parties under the terms of this License.
3168 c) If the modified program normally reads commands interactively
3169 when run, you must cause it, when started running for such
3170 interactive use in the most ordinary way, to print or display an
3171 announcement including an appropriate copyright notice and a
3172 notice that there is no warranty (or else, saying that you provide
3173 a warranty) and that users may redistribute the program under
3174 these conditions, and telling the user how to view a copy of this
3175 License. (Exception: if the Program itself is interactive but
3176 does not normally print such an announcement, your work based on
3177 the Program is not required to print an announcement.)
3180 These requirements apply to the modified work as a whole. If
3181 identifiable sections of that work are not derived from the Program,
3182 and can be reasonably considered independent and separate works in
3183 themselves, then this License, and its terms, do not apply to those
3184 sections when you distribute them as separate works. But when you
3185 distribute the same sections as part of a whole which is a work based
3186 on the Program, the distribution of the whole must be on the terms of
3187 this License, whose permissions for other licensees extend to the
3188 entire whole, and thus to each and every part regardless of who wrote it.
3190 Thus, it is not the intent of this section to claim rights or contest
3191 your rights to work written entirely by you; rather, the intent is to
3192 exercise the right to control the distribution of derivative or
3193 collective works based on the Program.
3195 In addition, mere aggregation of another work not based on the Program
3196 with the Program (or with a work based on the Program) on a volume of
3197 a storage or distribution medium does not bring the other work under
3198 the scope of this License.
3200 3. You may copy and distribute the Program (or a work based on it,
3201 under Section 2) in object code or executable form under the terms of
3202 Sections 1 and 2 above provided that you also do one of the following:
3204 a) Accompany it with the complete corresponding machine-readable
3205 source code, which must be distributed under the terms of Sections
3206 1 and 2 above on a medium customarily used for software interchange; or,
3208 b) Accompany it with a written offer, valid for at least three
3209 years, to give any third party, for a charge no more than your
3210 cost of physically performing source distribution, a complete
3211 machine-readable copy of the corresponding source code, to be
3212 distributed under the terms of Sections 1 and 2 above on a medium
3213 customarily used for software interchange; or,
3215 c) Accompany it with the information you received as to the offer
3216 to distribute corresponding source code. (This alternative is
3217 allowed only for noncommercial distribution and only if you
3218 received the program in object code or executable form with such
3219 an offer, in accord with Subsection b above.)
3221 The source code for a work means the preferred form of the work for
3222 making modifications to it. For an executable work, complete source
3223 code means all the source code for all modules it contains, plus any
3224 associated interface definition files, plus the scripts used to
3225 control compilation and installation of the executable. However, as a
3226 special exception, the source code distributed need not include
3227 anything that is normally distributed (in either source or binary
3228 form) with the major components (compiler, kernel, and so on) of the
3229 operating system on which the executable runs, unless that component
3230 itself accompanies the executable.
3232 If distribution of executable or object code is made by offering
3233 access to copy from a designated place, then offering equivalent
3234 access to copy the source code from the same place counts as
3235 distribution of the source code, even though third parties are not
3236 compelled to copy the source along with the object code.
3239 4. You may not copy, modify, sublicense, or distribute the Program
3240 except as expressly provided under this License. Any attempt
3241 otherwise to copy, modify, sublicense or distribute the Program is
3242 void, and will automatically terminate your rights under this License.
3243 However, parties who have received copies, or rights, from you under
3244 this License will not have their licenses terminated so long as such
3245 parties remain in full compliance.
3247 5. You are not required to accept this License, since you have not
3248 signed it. However, nothing else grants you permission to modify or
3249 distribute the Program or its derivative works. These actions are
3250 prohibited by law if you do not accept this License. Therefore, by
3251 modifying or distributing the Program (or any work based on the
3252 Program), you indicate your acceptance of this License to do so, and
3253 all its terms and conditions for copying, distributing or modifying
3254 the Program or works based on it.
3256 6. Each time you redistribute the Program (or any work based on the
3257 Program), the recipient automatically receives a license from the
3258 original licensor to copy, distribute or modify the Program subject to
3259 these terms and conditions. You may not impose any further
3260 restrictions on the recipients' exercise of the rights granted herein.
3261 You are not responsible for enforcing compliance by third parties to
3264 7. If, as a consequence of a court judgment or allegation of patent
3265 infringement or for any other reason (not limited to patent issues),
3266 conditions are imposed on you (whether by court order, agreement or
3267 otherwise) that contradict the conditions of this License, they do not
3268 excuse you from the conditions of this License. If you cannot
3269 distribute so as to satisfy simultaneously your obligations under this
3270 License and any other pertinent obligations, then as a consequence you
3271 may not distribute the Program at all. For example, if a patent
3272 license would not permit royalty-free redistribution of the Program by
3273 all those who receive copies directly or indirectly through you, then
3274 the only way you could satisfy both it and this License would be to
3275 refrain entirely from distribution of the Program.
3277 If any portion of this section is held invalid or unenforceable under
3278 any particular circumstance, the balance of the section is intended to
3279 apply and the section as a whole is intended to apply in other
3282 It is not the purpose of this section to induce you to infringe any
3283 patents or other property right claims or to contest validity of any
3284 such claims; this section has the sole purpose of protecting the
3285 integrity of the free software distribution system, which is
3286 implemented by public license practices. Many people have made
3287 generous contributions to the wide range of software distributed
3288 through that system in reliance on consistent application of that
3289 system; it is up to the author/donor to decide if he or she is willing
3290 to distribute software through any other system and a licensee cannot
3293 This section is intended to make thoroughly clear what is believed to
3294 be a consequence of the rest of this License.
3297 8. If the distribution and/or use of the Program is restricted in
3298 certain countries either by patents or by copyrighted interfaces, the
3299 original copyright holder who places the Program under this License
3300 may add an explicit geographical distribution limitation excluding
3301 those countries, so that distribution is permitted only in or among
3302 countries not thus excluded. In such case, this License incorporates
3303 the limitation as if written in the body of this License.
3305 9. The Free Software Foundation may publish revised and/or new versions
3306 of the General Public License from time to time. Such new versions will
3307 be similar in spirit to the present version, but may differ in detail to
3308 address new problems or concerns.
3310 Each version is given a distinguishing version number. If the Program
3311 specifies a version number of this License which applies to it and "any
3312 later version", you have the option of following the terms and conditions
3313 either of that version or of any later version published by the Free
3314 Software Foundation. If the Program does not specify a version number of
3315 this License, you may choose any version ever published by the Free Software
3318 10. If you wish to incorporate parts of the Program into other free
3319 programs whose distribution conditions are different, write to the author
3320 to ask for permission. For software which is copyrighted by the Free
3321 Software Foundation, write to the Free Software Foundation; we sometimes
3322 make exceptions for this. Our decision will be guided by the two goals
3323 of preserving the free status of all derivatives of our free software and
3324 of promoting the sharing and reuse of software generally.
3328 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
3329 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
3330 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
3331 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
3332 OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
3333 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
3334 TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
3335 PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
3336 REPAIR OR CORRECTION.
3338 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
3339 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
3340 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
3341 INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
3342 OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
3343 TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
3344 YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
3345 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
3346 POSSIBILITY OF SUCH DAMAGES.
3350 def create_menu_and_toolbar
3353 mb = Gtk::MenuBar.new
3355 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
3356 filesubmenu = Gtk::Menu.new
3357 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
3358 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
3359 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3360 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
3361 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
3362 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3363 tooltips = Gtk::Tooltips.new
3364 filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
3365 $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3366 tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
3367 filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
3368 $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3369 tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
3370 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
3371 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3372 tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums (anywhere)")), nil)
3373 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3374 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
3375 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3376 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
3377 filesubmenu.append($view_wa = Gtk::ImageMenuItem.new(utf8(_("View web-album with browser"))).set_sensitive(false))
3378 $view_wa.image = Gtk::Image.new("#{$FPATH}/images/stock-view-webalbum-16.png")
3379 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3380 filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
3381 tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
3382 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3383 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
3384 filemenu.set_submenu(filesubmenu)
3387 new.signal_connect('activate') { new_album }
3388 open.signal_connect('activate') { open_file_popup }
3389 $save.signal_connect('activate') { save_current_file_user }
3390 $save_as.signal_connect('activate') { save_as_do }
3391 $merge_current.signal_connect('activate') { merge_current }
3392 $merge_newsubs.signal_connect('activate') { merge_newsubs }
3393 $merge.signal_connect('activate') { merge }
3394 $generate.signal_connect('activate') {
3396 call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
3397 utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
3399 { :successmsg => utf8(_("Your web-album is now ready in directory '%s'.
3400 Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
3401 :successmsg_linkurl => $xmldoc.root.attributes['destination'],
3402 :closure_after => proc {
3403 $xmldoc.elements.each('//dir') { |elem|
3404 elem.add_attribute('already-generated', 'true')
3406 UndoHandler.cleanup #- prevent save_changes to mark current dir as not already generated
3407 $undo_tb.sensitive = $undo_mb.sensitive = false
3408 $redo_tb.sensitive = $redo_mb.sensitive = false
3410 $generated_outofline = true
3413 $view_wa.signal_connect('activate') {
3414 indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
3415 if File.exists?(indexhtml)
3418 show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
3421 $properties.signal_connect('activate') { properties }
3423 quit.signal_connect('activate') { try_quit }
3425 editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
3426 editsubmenu = Gtk::Menu.new
3427 editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
3428 editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
3429 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3430 editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
3431 $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
3432 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)
3433 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3434 editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
3435 editmenu.set_submenu(editsubmenu)
3438 $remove_all_captions.signal_connect('activate') { remove_all_captions }
3440 prefs.signal_connect('activate') { preferences }
3442 helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
3443 helpsubmenu = Gtk::Menu.new
3444 helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
3445 one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
3446 helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
3447 speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
3448 helpsubmenu.append(tutos = Gtk::ImageMenuItem.new(utf8(_("Online tutorials (opens a web-browser)"))))
3449 tutos.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3450 helpsubmenu.append( Gtk::SeparatorMenuItem.new)
3451 helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
3452 helpmenu.set_submenu(helpsubmenu)
3455 one_click.signal_connect('activate') {
3456 show_one_click_explanation(_("One-Click tools are available in the toolbar."))
3459 speed.signal_connect('activate') {
3460 show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
3462 <span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
3463 <span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
3464 <span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
3465 <span foreground='darkblue'>Control-Enter</span>: for an image, open larger view; for a video, launch player
3466 <span foreground='darkblue'>Control-Delete</span>: delete image
3467 <span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
3468 <span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
3469 <span foreground='darkblue'>Control-z</span>: undo
3470 <span foreground='darkblue'>Control-r</span>: redo
3472 <span size='large' weight='bold'>Mouse gestures:</span>
3474 Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
3475 for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
3477 <span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
3478 <span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
3479 <span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
3480 <span foreground='darkblue'>Left click, hold left button, right click</span>: undo
3481 <span foreground='darkblue'>Right click, hold right button, left click</span>: redo
3482 ")), { :pos_centered => true, :not_transient => true })
3485 tutos.signal_connect('activate') {
3486 open_url('http://zarb.org/~gc/html/booh/tutorial.html')
3489 about.signal_connect('activate') {
3490 Gtk::AboutDialog.set_url_hook { |dialog, url| open_url(url) }
3491 Gtk::AboutDialog.show($main_window, { :name => 'booh',
3492 :version => $VERSION,
3493 :copyright => 'Copyright (c) 2005 Guillaume Cottenceau',
3494 :license => get_license,
3495 :website => 'http://zarb.org/~gc/html/booh.html',
3496 :authors => [ 'Guillaume Cottenceau' ],
3497 :artists => [ 'Ayo73' ],
3498 :comments => utf8(_("''The Web-Album of choice for discriminating Linux users''")),
3499 :translator_credits => utf8(_('Japanese: Masao Mutoh