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.attributes["#{attributes_prefix}white-balance"] || "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.
649 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
650 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
652 dialog.window_position = Gtk::Window::POS_MOUSE
656 timeout = Gtk.timeout_add(100) {
657 if hs.value != lastval
659 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
664 dialog.run { |response|
665 Gtk.timeout_remove(timeout)
666 if response == Gtk::Dialog::RESPONSE_OK
668 newval = hs.value.to_s
669 msg 3, "changing white balance to #{newval}"
671 return { :old => value, :new => newval }
673 $modified_pixbufs[thumbnail_img] ||= {}
674 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
675 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
682 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
683 system("rm -f '#{destfile}'")
684 #- type can be 'element' or 'subdir'
686 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
688 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
692 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
694 push_mousecursor_wait
695 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
697 puts "destroyed: " + img.destroyed?.to_s
699 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
705 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
706 distribute_multiple_call = Proc.new { |action, arg|
707 $selected_elements.each_key { |path|
708 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
710 if possible_actions[:can_multiple] && $selected_elements.length > 0
711 UndoHandler.begin_batch
712 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
713 UndoHandler.end_batch
715 closures[action].call(arg)
717 $selected_elements = {}
720 if optionals.include?('change_image')
721 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
722 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
723 changeimg.signal_connect('activate') { closures[:change].call }
724 menu.append( Gtk::SeparatorMenuItem.new)
728 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
729 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
730 view.signal_connect('activate') { closures[:view].call }
732 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
733 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
734 view.signal_connect('activate') { closures[:view].call }
735 menu.append( Gtk::SeparatorMenuItem.new)
739 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
740 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
741 exif.signal_connect('activate') { show_popup($main_window,
742 utf8(`identify -format "%[EXIF:*]" #{fullpath}`.sub(/MakerNote.*\n/, '')),
743 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
744 menu.append( Gtk::SeparatorMenuItem.new)
746 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
747 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
748 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
749 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
750 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
751 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
752 if !possible_actions[:can_multiple] || $selected_elements.length == 0
753 menu.append( Gtk::SeparatorMenuItem.new)
754 if !possible_actions[:forbid_left]
755 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
756 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
757 moveleft.signal_connect('activate') { closures[:move].call('left') }
758 if !possible_actions[:can_left]
759 moveleft.sensitive = false
762 if !possible_actions[:forbid_right]
763 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
764 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
765 moveright.signal_connect('activate') { closures[:move].call('right') }
766 if !possible_actions[:can_right]
767 moveright.sensitive = false
770 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
771 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
772 moveup.signal_connect('activate') { closures[:move].call('up') }
773 if !possible_actions[:can_up]
774 moveup.sensitive = false
776 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
777 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
778 movedown.signal_connect('activate') { closures[:move].call('down') }
779 if !possible_actions[:can_down]
780 movedown.sensitive = false
784 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
785 menu.append( Gtk::SeparatorMenuItem.new)
786 menu.append( color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
787 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
788 color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
789 menu.append( flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
790 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
791 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
792 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
793 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
794 frame_offset.signal_connect('activate') {
795 if possible_actions[:can_multiple] && $selected_elements.length > 0
796 if values = ask_new_frame_offset(nil, '')
797 distribute_multiple_call.call(:frame_offset, values)
800 closures[:frame_offset].call
805 menu.append( Gtk::SeparatorMenuItem.new)
806 if !possible_actions[:can_multiple] || $selected_elements.length == 0
807 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
808 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
809 whitebalance.signal_connect('activate') { closures[:whitebalance].call }
811 if !possible_actions[:can_multiple] || $selected_elements.length == 0
812 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
813 _("Enhance constrast"))))
815 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
817 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
818 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
819 if type == 'image' && possible_actions[:can_panorama]
820 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
821 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
822 panorama.signal_connect('activate') { closures[:pano].call }
824 if optionals.include?('delete')
825 menu.append( Gtk::SeparatorMenuItem.new)
826 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
827 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
828 if !possible_actions[:can_multiple] || $selected_elements.length == 0
829 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
830 paste_item.signal_connect('activate') { closures[:paste].call }
831 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
832 clear_item.signal_connect('activate') { $cuts = [] }
834 paste_item.sensitive = clear_item.sensitive = false
837 menu.append( Gtk::SeparatorMenuItem.new)
838 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
839 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
842 menu.popup(nil, nil, event.button, event.time)
845 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
848 frame1 = Gtk::Frame.new
849 fullpath = from_utf8("#{$current_path}/#{filename}")
851 my_gen_real_thumbnail = proc {
852 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
855 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
856 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
857 frame1.add(img = Gtk::Image.new)
858 my_gen_real_thumbnail.call
860 frame1.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
862 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
864 tooltips = Gtk::Tooltips.new
865 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
866 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
868 frame2, textview = create_editzone($autotable_sw, 1, img)
869 textview.buffer.text = utf8(caption)
870 textview.set_justification(Gtk::Justification::CENTER)
872 vbox = Gtk::VBox.new(false, 5)
873 vbox.pack_start(evtbox, false, false)
874 vbox.pack_start(frame2, false, false)
875 autotable.append(vbox, filename)
877 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
878 $vbox2widgets[vbox] = { :textview => textview, :image => img }
880 #- to be able to find widgets by name
881 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
883 cleanup_all_thumbnails = Proc.new {
884 #- remove out of sync images
885 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
886 for sizeobj in $images_size
887 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
892 rotate_and_cleanup = Proc.new { |angle|
893 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
894 cleanup_all_thumbnails.call
897 move = Proc.new { |direction|
898 do_method = "move_#{direction}"
899 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
901 done = autotable.method(do_method).call(vbox)
902 textview.grab_focus #- because if moving, focus is stolen
906 save_undo(_("move %s") % direction,
908 autotable.method(undo_method).call(vbox)
909 textview.grab_focus #- because if moving, focus is stolen
910 autoscroll_if_needed($autotable_sw, img, textview)
911 $notebook.set_page(1)
913 autotable.method(do_method).call(vbox)
914 textview.grab_focus #- because if moving, focus is stolen
915 autoscroll_if_needed($autotable_sw, img, textview)
916 $notebook.set_page(1)
922 color_swap_and_cleanup = Proc.new {
923 perform_color_swap_and_cleanup = Proc.new {
924 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
925 my_gen_real_thumbnail.call
928 cleanup_all_thumbnails.call
929 perform_color_swap_and_cleanup.call
931 save_undo(_("color swap"),
933 perform_color_swap_and_cleanup.call
935 autoscroll_if_needed($autotable_sw, img, textview)
936 $notebook.set_page(1)
938 perform_color_swap_and_cleanup.call
940 autoscroll_if_needed($autotable_sw, img, textview)
941 $notebook.set_page(1)
946 change_frame_offset_and_cleanup_real = Proc.new { |values|
947 perform_change_frame_offset_and_cleanup = Proc.new { |val|
948 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
949 my_gen_real_thumbnail.call
951 perform_change_frame_offset_and_cleanup.call(values[:new])
953 save_undo(_("specify frame offset"),
955 perform_change_frame_offset_and_cleanup.call(values[:old])
957 autoscroll_if_needed($autotable_sw, img, textview)
958 $notebook.set_page(1)
960 perform_change_frame_offset_and_cleanup.call(values[:new])
962 autoscroll_if_needed($autotable_sw, img, textview)
963 $notebook.set_page(1)
968 change_frame_offset_and_cleanup = Proc.new {
969 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
970 change_frame_offset_and_cleanup_real.call(values)
974 change_pano_amount_and_cleanup_real = Proc.new { |values|
975 perform_change_pano_amount_and_cleanup = Proc.new { |val|
976 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
978 perform_change_pano_amount_and_cleanup.call(values[:new])
980 save_undo(_("change panorama amount"),
982 perform_change_pano_amount_and_cleanup.call(values[:old])
984 autoscroll_if_needed($autotable_sw, img, textview)
985 $notebook.set_page(1)
987 perform_change_pano_amount_and_cleanup.call(values[:new])
989 autoscroll_if_needed($autotable_sw, img, textview)
990 $notebook.set_page(1)
995 change_pano_amount_and_cleanup = Proc.new {
996 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
997 change_pano_amount_and_cleanup_real.call(values)
1001 whitebalance_and_cleanup = Proc.new {
1002 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1003 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1004 perform_change_whitebalance_and_cleanup = Proc.new { |val|
1005 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1006 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1007 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1008 cleanup_all_thumbnails.call
1010 perform_change_whitebalance_and_cleanup.call(values[:new])
1012 save_undo(_("fix white balance"),
1014 perform_change_whitebalance_and_cleanup.call(values[:old])
1016 autoscroll_if_needed($autotable_sw, img, textview)
1017 $notebook.set_page(1)
1019 perform_change_whitebalance_and_cleanup.call(values[:new])
1021 autoscroll_if_needed($autotable_sw, img, textview)
1022 $notebook.set_page(1)
1028 enhance_and_cleanup = Proc.new {
1029 perform_enhance_and_cleanup = Proc.new {
1030 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1031 my_gen_real_thumbnail.call
1034 cleanup_all_thumbnails.call
1035 perform_enhance_and_cleanup.call
1037 save_undo(_("enhance"),
1039 perform_enhance_and_cleanup.call
1041 autoscroll_if_needed($autotable_sw, img, textview)
1042 $notebook.set_page(1)
1044 perform_enhance_and_cleanup.call
1046 autoscroll_if_needed($autotable_sw, img, textview)
1047 $notebook.set_page(1)
1052 delete = Proc.new { |isacut|
1053 if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed?")), { :okcancel => true })
1056 perform_delete = Proc.new {
1057 after = autotable.get_next_widget(vbox)
1059 after = autotable.get_previous_widget(vbox)
1061 if $config['deleteondisk'] && !isacut
1062 msg 3, "scheduling for delete: #{fullpath}"
1063 $todelete << fullpath
1065 autotable.remove(vbox)
1067 $vbox2widgets[after][:textview].grab_focus
1068 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1072 previous_pos = autotable.get_current_number(vbox)
1076 if $xmldir.child_byname_notattr('dir', 'deleted')
1077 $xmldir.delete_attribute('thumbnails-caption')
1078 $xmldir.delete_attribute('thumbnails-captionfile')
1080 $xmldir.add_attribute('deleted', 'true')
1082 while moveup.parent.name == 'dir'
1083 moveup = moveup.parent
1084 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1085 moveup.add_attribute('deleted', 'true')
1091 save_changes('forced')
1092 populate_subalbums_treeview
1094 save_undo(_("delete"),
1096 autotable.reinsert(pos, vbox, filename)
1097 $notebook.set_page(1)
1098 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1100 msg 3, "removing deletion schedule of: #{fullpath}"
1101 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1104 $notebook.set_page(1)
1113 $cuts << { :vbox => vbox, :filename => filename }
1114 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1119 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1122 autotable.queue_draws << proc {
1123 $vbox2widgets[last[:vbox]][:textview].grab_focus
1124 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1126 save_undo(_("paste"),
1128 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1129 $notebook.set_page(1)
1132 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1134 $notebook.set_page(1)
1137 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1142 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1143 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1144 :pano => change_pano_amount_and_cleanup }
1146 textview.signal_connect('key-press-event') { |w, event|
1149 x, y = autotable.get_current_pos(vbox)
1150 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1151 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1152 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1153 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1155 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1156 $vbox2widgets[widget_up][:textview].grab_focus
1163 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1165 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1166 $vbox2widgets[widget_down][:textview].grab_focus
1173 if event.keyval == Gdk::Keyval::GDK_Left
1176 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1183 rotate_and_cleanup.call(-90)
1186 if event.keyval == Gdk::Keyval::GDK_Right
1187 next_ = autotable.get_next_widget(vbox)
1188 if next_ && autotable.get_current_pos(next_)[0] > x
1190 $vbox2widgets[next_][:textview].grab_focus
1197 rotate_and_cleanup.call(90)
1200 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1203 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1204 view_element(filename, { :delete => delete })
1207 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1210 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1214 !propagate #- propagate if needed
1217 $ignore_next_release = false
1218 evtbox.signal_connect('button-press-event') { |w, event|
1219 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1220 if event.state & Gdk::Window::BUTTON3_MASK != 0
1221 #- gesture redo: hold right mouse button then click left mouse button
1222 $config['nogestures'] or perform_redo
1223 $ignore_next_release = true
1225 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1227 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1229 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1230 elsif $enhance.active?
1231 enhance_and_cleanup.call
1232 elsif $delete.active?
1236 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1239 $button1_pressed_autotable = true
1240 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1241 if event.state & Gdk::Window::BUTTON1_MASK != 0
1242 #- gesture undo: hold left mouse button then click right mouse button
1243 $config['nogestures'] or perform_undo
1244 $ignore_next_release = true
1246 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1247 view_element(filename, { :delete => delete })
1252 evtbox.signal_connect('button-release-event') { |w, event|
1253 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1254 if !$ignore_next_release
1255 x, y = autotable.get_current_pos(vbox)
1256 next_ = autotable.get_next_widget(vbox)
1257 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1258 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1259 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1260 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1261 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1262 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1263 :pano => change_pano_amount_and_cleanup })
1265 $ignore_next_release = false
1266 $gesture_press = nil
1271 #- handle reordering with drag and drop
1272 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1273 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1274 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1275 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1278 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1280 #- mouse gesture first (dnd disables button-release-event)
1281 if $gesture_press && $gesture_press[:filename] == filename
1282 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1283 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1284 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1285 rotate_and_cleanup.call(angle)
1286 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1288 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1289 msg 3, "gesture delete: click-drag right button to the bottom"
1291 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1296 ctxt.targets.each { |target|
1297 if target.name == 'reorder-elements'
1298 move_dnd = Proc.new { |from,to|
1301 autotable.move(from, to)
1302 save_undo(_("reorder"),
1303 Proc.new { |from, to|
1305 autotable.move(to - 1, from)
1307 autotable.move(to, from + 1)
1309 $notebook.set_page(1)
1311 autotable.move(from, to)
1312 $notebook.set_page(1)
1317 if $multiple_dnd.size == 0
1318 move_dnd.call(selection_data.data.to_i,
1319 autotable.get_current_number(vbox))
1321 UndoHandler.begin_batch
1322 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1324 #- need to update current position between each call
1325 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1326 autotable.get_current_number(vbox))
1328 UndoHandler.end_batch
1339 def create_auto_table
1341 $autotable = Gtk::AutoTable.new(5)
1343 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1344 thumbnails_vb = Gtk::VBox.new(false, 5)
1346 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1347 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1348 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1349 thumbnails_vb.add($autotable)
1351 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1352 $autotable_sw.add_with_viewport(thumbnails_vb)
1354 #- follows stuff for handling multiple elements selection
1355 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1357 update_selected = Proc.new {
1358 $autotable.current_order.each { |path|
1359 w = $name2widgets[path][:evtbox].window
1360 xm = w.position[0] + w.size[0]/2
1361 ym = w.position[1] + w.size[1]/2
1362 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1363 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1364 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1365 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1368 if $selected_elements[path] && ! $selected_elements[path][:keep]
1369 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))
1370 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1371 $selected_elements.delete(path)
1376 $autotable.signal_connect('realize') { |w,e|
1377 gc = Gdk::GC.new($autotable.window)
1378 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1379 gc.function = Gdk::GC::INVERT
1380 #- autoscroll handling for DND and multiple selections
1381 Gtk.timeout_add(100) {
1382 w, x, y, mask = $autotable.window.pointer
1383 if mask & Gdk::Window::BUTTON1_MASK != 0
1384 if y < $autotable_sw.vadjustment.value
1386 $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]])
1388 if $button1_pressed_autotable || press_x
1389 scroll_upper($autotable_sw, y)
1392 w, pos_x, pos_y = $autotable.window.pointer
1393 $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]])
1394 update_selected.call
1397 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1399 $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]])
1401 if $button1_pressed_autotable || press_x
1402 scroll_lower($autotable_sw, y)
1405 w, pos_x, pos_y = $autotable.window.pointer
1406 $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]])
1407 update_selected.call
1415 $autotable.signal_connect('button-press-event') { |w,e|
1417 if !$button1_pressed_autotable
1420 if e.state & Gdk::Window::SHIFT_MASK == 0
1421 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1422 $selected_elements = {}
1423 $statusbar.push(0, utf8(_("Nothing selected.")))
1425 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1427 set_mousecursor(Gdk::Cursor::TCROSS)
1431 $autotable.signal_connect('button-release-event') { |w,e|
1433 if $button1_pressed_autotable
1434 #- unselect all only now
1435 $multiple_dnd = $selected_elements.keys
1436 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1437 $selected_elements = {}
1438 $button1_pressed_autotable = false
1441 $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]])
1442 if $selected_elements.length > 0
1443 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1446 press_x = press_y = pos_x = pos_y = nil
1447 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1451 $autotable.signal_connect('motion-notify-event') { |w,e|
1454 $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]])
1458 $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]])
1459 update_selected.call
1465 def create_subalbums_page
1467 subalbums_hb = Gtk::HBox.new
1468 $subalbums_vb = Gtk::VBox.new(false, 5)
1469 subalbums_hb.pack_start($subalbums_vb, false, false)
1470 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1471 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1472 $subalbums_sw.add_with_viewport(subalbums_hb)
1475 def save_current_file
1479 ios = File.open($filename, "w")
1480 $xmldoc.write(ios, 0)
1485 def save_current_file_user
1486 save_tempfilename = $filename
1487 $filename = $orig_filename
1490 $generated_outofline = false
1491 $filename = save_tempfilename
1493 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1494 $todelete.each { |f|
1495 system("rm -f #{f}")
1499 def mark_document_as_dirty
1500 $xmldoc.elements.each('//dir') { |elem|
1501 elem.delete_attribute('already-generated')
1505 #- ret: true => ok false => cancel
1506 def ask_save_modifications(msg1, msg2, *options)
1508 options = options.size > 0 ? options[0] : {}
1510 if options[:disallow_cancel]
1511 dialog = Gtk::Dialog.new(msg1,
1513 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1514 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1515 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1517 dialog = Gtk::Dialog.new(msg1,
1519 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1520 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1521 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1522 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1524 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1525 dialog.vbox.add(Gtk::Label.new(msg2))
1526 dialog.window_position = Gtk::Window::POS_CENTER
1529 dialog.run { |response|
1531 if response == Gtk::Dialog::RESPONSE_YES
1532 save_current_file_user
1534 #- if we have generated an album but won't save modifications, we must remove
1535 #- already-generated markers in original file
1536 if $generated_outofline
1538 $xmldoc = REXML::Document.new File.new($orig_filename)
1539 mark_document_as_dirty
1540 ios = File.open($orig_filename, "w")
1541 $xmldoc.write(ios, 0)
1544 puts "exception: #{$!}"
1548 if response == Gtk::Dialog::RESPONSE_CANCEL
1551 $todelete = [] #- unconditionally clear the list of images/videos to delete
1557 def try_quit(*options)
1558 if ask_save_modifications(utf8(_("Save before quitting?")),
1559 utf8(_("Do you want to save your changes before quitting?")),
1565 def show_popup(parent, msg, *options)
1566 dialog = Gtk::Dialog.new
1567 if options[0] && options[0][:title]
1568 dialog.title = options[0][:title]
1570 dialog.title = utf8(_("Booh message"))
1572 lbl = Gtk::Label.new
1573 if options[0] && options[0][:nomarkup]
1578 if options[0] && options[0][:centered]
1579 lbl.set_justify(Gtk::Justification::CENTER)
1581 if options[0] && options[0][:selectable]
1582 lbl.selectable = true
1584 if options[0] && options[0][:topwidget]
1585 dialog.vbox.add(options[0][:topwidget])
1587 if options[0] && options[0][:scrolled]
1588 sw = Gtk::ScrolledWindow.new(nil, nil)
1589 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1590 sw.add_with_viewport(lbl)
1592 dialog.set_default_size(400, 500)
1594 dialog.vbox.add(lbl)
1595 dialog.set_default_size(200, 120)
1597 if options[0] && options[0][:okcancel]
1598 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1600 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1602 if options[0] && options[0][:pos_centered]
1603 dialog.window_position = Gtk::Window::POS_CENTER
1605 dialog.window_position = Gtk::Window::POS_MOUSE
1608 if options[0] && options[0][:linkurl]
1609 linkbut = Gtk::Button.new('')
1610 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1611 linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1612 linkbut.relief = Gtk::RELIEF_NONE
1613 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1614 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1615 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1620 if !options[0] || !options[0][:not_transient]
1621 dialog.transient_for = parent
1622 dialog.run { |response|
1624 if options[0] && options[0][:okcancel]
1625 return response == Gtk::Dialog::RESPONSE_OK
1629 dialog.signal_connect('response') { dialog.destroy }
1633 def backend_wait_message(parent, msg, infopipe_path, mode)
1635 w.set_transient_for(parent)
1638 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1639 vb.pack_start(Gtk::Label.new(msg), false, false)
1641 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1642 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1643 if mode != 'one dir scan'
1644 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1646 if mode == 'web-album'
1647 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1648 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1650 vb.pack_start(Gtk::HSeparator.new, false, false)
1652 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1653 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1654 vb.pack_end(bottom, false, false)
1656 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1657 refresh_thread = Thread.new {
1658 directories_counter = 0
1659 while line = infopipe.gets
1660 if line =~ /^directories: (\d+), sizes: (\d+)/
1661 directories = $1.to_f + 1
1663 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1664 elements = $3.to_f + 1
1665 if mode == 'web-album'
1669 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = 0 }
1670 if mode != 'one dir scan'
1671 newtext = utf8(full_src_dir_to_rel($1, $2))
1672 newtext = '/' if newtext == ''
1673 gtk_thread_protect { puts "destroyed: " + pb1_2.destroyed?.to_s; pb1_2.text = newtext }
1674 directories_counter += 1
1675 gtk_thread_protect { puts "destroyed: " + pb1_2.destroyed?.to_s; pb1_2.fraction = directories_counter / directories }
1677 elsif line =~ /^processing element$/
1678 element_counter += 1
1679 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = element_counter / elements }
1680 elsif line =~ /^processing size$/
1681 element_counter += 1
1682 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = element_counter / elements }
1683 elsif line =~ /^finished processing sizes$/
1684 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = 1 }
1685 elsif line =~ /^creating index.html$/
1686 gtk_thread_protect { puts "destroyed: " + pb1_2.destroyed?.to_s; pb1_2.text = utf8(_("finished")) }
1687 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = pb1_2.fraction = 1 }
1688 directories_counter = 0
1689 elsif line =~ /^index.html: (.+)\|(.+)/
1690 newtext = utf8(full_src_dir_to_rel($1, $2))
1691 newtext = '/' if newtext == ''
1692 gtk_thread_protect { puts "destroyed: " + pb2.destroyed?.to_s; pb2.text = newtext }
1693 directories_counter += 1
1694 gtk_thread_protect { puts "destroyed: " + pb2.destroyed?.to_s; pb2.fraction = directories_counter / directories }
1695 elsif line =~ /^die: (.*)$/
1702 w.signal_connect('delete-event') { w.destroy }
1703 w.signal_connect('destroy') {
1704 Thread.kill(refresh_thread)
1705 gtk_thread_abandon #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1708 system("rm -f #{infopipe_path}")
1711 w.window_position = Gtk::Window::POS_CENTER
1717 def call_backend(cmd, waitmsg, mode, params)
1718 pipe = Tempfile.new("boohpipe")
1720 system("mkfifo #{pipe.path}")
1721 cmd += " --info-pipe #{pipe.path}"
1722 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
1727 id, exitstatus = Process.waitpid2(pid)
1728 gtk_thread_protect { puts "destroyed: " + w8.destroyed?.to_s; w8.destroy }
1730 if params[:successmsg]
1731 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
1733 if params[:closure_after]
1734 gtk_thread_protect(¶ms[:closure_after])
1736 elsif exitstatus == 15
1737 #- say nothing, user aborted
1739 gtk_thread_protect { show_popup($main_window,
1740 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
1746 button.signal_connect('clicked') {
1747 Process.kill('SIGTERM', pid)
1751 def save_changes(*forced)
1752 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1756 $xmldir.delete_attribute('already-generated')
1758 propagate_children = Proc.new { |xmldir|
1759 if xmldir.attributes['subdirs-caption']
1760 xmldir.delete_attribute('already-generated')
1762 xmldir.elements.each('dir') { |element|
1763 propagate_children.call(element)
1767 if $xmldir.child_byname_notattr('dir', 'deleted')
1768 new_title = $subalbums_title.buffer.text
1769 if new_title != $xmldir.attributes['subdirs-caption']
1770 parent = $xmldir.parent
1771 if parent.name == 'dir'
1772 parent.delete_attribute('already-generated')
1774 propagate_children.call($xmldir)
1776 $xmldir.add_attribute('subdirs-caption', new_title)
1777 $xmldir.elements.each('dir') { |element|
1778 if !element.attributes['deleted']
1779 path = element.attributes['path']
1780 newtext = $subalbums_edits[path][:editzone].buffer.text
1781 if element.attributes['subdirs-caption']
1782 if element.attributes['subdirs-caption'] != newtext
1783 propagate_children.call(element)
1785 element.add_attribute('subdirs-caption', newtext)
1786 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1788 if element.attributes['thumbnails-caption'] != newtext
1789 element.delete_attribute('already-generated')
1791 element.add_attribute('thumbnails-caption', newtext)
1792 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1798 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
1799 if $xmldir.attributes['thumbnails-caption']
1800 path = $xmldir.attributes['path']
1801 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1803 elsif $xmldir.attributes['thumbnails-caption']
1804 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
1807 #- remove and reinsert elements to reflect new ordering
1810 $xmldir.elements.each { |element|
1811 if element.name == 'image' || element.name == 'video'
1812 saves[element.attributes['filename']] = element.remove
1816 $autotable.current_order.each { |path|
1817 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1818 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1821 saves.each_key { |path|
1822 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1823 chld.add_attribute('deleted', 'true')
1827 def remove_all_captions
1830 $autotable.current_order.each { |path|
1831 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
1832 $name2widgets[File.basename(path)][:textview].buffer.text = ''
1834 save_undo(_("remove all captions"),
1836 texts.each_key { |key|
1837 $name2widgets[key][:textview].buffer.text = texts[key]
1839 $notebook.set_page(1)
1841 texts.each_key { |key|
1842 $name2widgets[key][:textview].buffer.text = ''
1844 $notebook.set_page(1)
1850 $selected_elements.each_key { |path|
1851 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1857 $selected_elements = {}
1861 $undo_tb.sensitive = $undo_mb.sensitive = false
1862 $redo_tb.sensitive = $redo_mb.sensitive = false
1868 $subalbums_vb.children.each { |chld|
1869 $subalbums_vb.remove(chld)
1871 $subalbums = Gtk::Table.new(0, 0, true)
1872 current_y_sub_albums = 0
1874 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
1875 $subalbums_edits = {}
1876 subalbums_counter = 0
1877 subalbums_edits_bypos = {}
1879 add_subalbum = Proc.new { |xmldir, counter|
1880 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
1881 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
1882 if xmldir == $xmldir
1883 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
1884 caption = xmldir.attributes['thumbnails-caption']
1885 captionfile, dummy = find_subalbum_caption_info(xmldir)
1886 infotype = 'thumbnails'
1888 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
1889 captionfile, caption = find_subalbum_caption_info(xmldir)
1890 infotype = find_subalbum_info_type(xmldir)
1892 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
1893 hbox = Gtk::HBox.new
1894 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
1896 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
1899 my_gen_real_thumbnail = proc {
1900 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
1903 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
1904 f.add(img = Gtk::Image.new)
1905 my_gen_real_thumbnail.call
1907 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
1909 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
1910 $subalbums.attach(hbox,
1911 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1913 frame, textview = create_editzone($subalbums_sw, 0, img)
1914 textview.buffer.text = caption
1915 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
1916 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1918 change_image = Proc.new {
1919 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
1921 Gtk::FileChooser::ACTION_OPEN,
1923 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1924 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
1925 fc.transient_for = $main_window
1926 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))
1927 f.add(preview_img = Gtk::Image.new)
1929 fc.signal_connect('update-preview') { |w|
1931 if fc.preview_filename
1932 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
1933 fc.preview_widget_active = true
1935 rescue Gdk::PixbufError
1936 fc.preview_widget_active = false
1939 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1941 old_file = captionfile
1942 old_rotate = xmldir.attributes["#{infotype}-rotate"]
1943 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
1944 old_enhance = xmldir.attributes["#{infotype}-enhance"]
1945 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
1947 new_file = fc.filename
1948 msg 3, "new captionfile is: #{fc.filename}"
1949 perform_changefile = Proc.new {
1950 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
1951 $modified_pixbufs.delete(thumbnail_file)
1952 xmldir.delete_attribute("#{infotype}-rotate")
1953 xmldir.delete_attribute("#{infotype}-color-swap")
1954 xmldir.delete_attribute("#{infotype}-enhance")
1955 xmldir.delete_attribute("#{infotype}-frame-offset")
1956 my_gen_real_thumbnail.call
1958 perform_changefile.call
1960 save_undo(_("change caption file for sub-album"),
1962 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
1963 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
1964 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
1965 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
1966 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
1967 my_gen_real_thumbnail.call
1968 $notebook.set_page(0)
1970 perform_changefile.call
1971 $notebook.set_page(0)
1978 rotate_and_cleanup = Proc.new { |angle|
1979 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
1980 system("rm -f '#{thumbnail_file}'")
1983 move = Proc.new { |direction|
1986 save_changes('forced')
1987 if direction == 'up'
1988 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
1989 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
1990 subalbums_edits_bypos[oldpos - 1][:position] += 1
1992 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
1993 $subalbums_edits[xmldir.attributes['path']][:position] += 1
1994 subalbums_edits_bypos[oldpos + 1][:position] -= 1
1998 $xmldir.elements.each('dir') { |element|
1999 if (!element.attributes['deleted'])
2000 elems << [ element.attributes['path'], element.remove ]
2003 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2004 each { |e| $xmldir.add_element(e[1]) }
2005 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2006 $xmldir.elements.each('descendant::dir') { |elem|
2007 elem.delete_attribute('already-generated')
2012 color_swap_and_cleanup = Proc.new {
2013 perform_color_swap_and_cleanup = Proc.new {
2014 color_swap(xmldir, "#{infotype}-")
2015 my_gen_real_thumbnail.call
2017 perform_color_swap_and_cleanup.call
2019 save_undo(_("color swap"),
2021 perform_color_swap_and_cleanup.call
2022 $notebook.set_page(0)
2024 perform_color_swap_and_cleanup.call
2025 $notebook.set_page(0)
2030 change_frame_offset_and_cleanup = Proc.new {
2031 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2032 perform_change_frame_offset_and_cleanup = Proc.new { |val|
2033 change_frame_offset(xmldir, "#{infotype}-", val)
2034 my_gen_real_thumbnail.call
2036 perform_change_frame_offset_and_cleanup.call(values[:new])
2038 save_undo(_("specify frame offset"),
2040 perform_change_frame_offset_and_cleanup.call(values[:old])
2041 $notebook.set_page(0)
2043 perform_change_frame_offset_and_cleanup.call(values[:new])
2044 $notebook.set_page(0)
2050 whitebalance_and_cleanup = Proc.new {
2051 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2052 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2053 perform_change_whitebalance_and_cleanup = Proc.new { |val|
2054 change_whitebalance(xmldir, "#{infotype}-", val)
2055 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2056 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2057 system("rm -f '#{thumbnail_file}'")
2059 perform_change_whitebalance_and_cleanup.call(values[:new])
2061 save_undo(_("fix white balance"),
2063 perform_change_whitebalance_and_cleanup.call(values[:old])
2064 $notebook.set_page(0)
2066 perform_change_whitebalance_and_cleanup.call(values[:new])
2067 $notebook.set_page(0)
2073 enhance_and_cleanup = Proc.new {
2074 perform_enhance_and_cleanup = Proc.new {
2075 enhance(xmldir, "#{infotype}-")
2076 my_gen_real_thumbnail.call
2079 perform_enhance_and_cleanup.call
2081 save_undo(_("enhance"),
2083 perform_enhance_and_cleanup.call
2084 $notebook.set_page(0)
2086 perform_enhance_and_cleanup.call
2087 $notebook.set_page(0)
2092 evtbox.signal_connect('button-press-event') { |w, event|
2093 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2095 rotate_and_cleanup.call(90)
2097 rotate_and_cleanup.call(-90)
2098 elsif $enhance.active?
2099 enhance_and_cleanup.call
2102 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2103 popup_thumbnail_menu(event, ['change_image'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2104 { :forbid_left => true, :forbid_right => true,
2105 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter },
2106 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2107 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
2109 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2114 evtbox.signal_connect('button-press-event') { |w, event|
2115 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2119 evtbox.signal_connect('button-release-event') { |w, event|
2120 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2121 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2122 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2123 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2124 msg 3, "gesture rotate: #{angle}"
2125 rotate_and_cleanup.call(angle)
2128 $gesture_press = nil
2131 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2132 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2133 current_y_sub_albums += 1
2136 if $xmldir.child_byname_notattr('dir', 'deleted')
2138 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2139 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2140 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2141 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2142 #- this album image/caption
2143 if $xmldir.attributes['thumbnails-caption']
2144 add_subalbum.call($xmldir, 0)
2147 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2148 $xmldir.elements.each { |element|
2149 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2150 #- element (image or video) of this album
2151 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2152 msg 3, "dest_img: #{dest_img}"
2153 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
2154 total[element.name] += 1
2156 if element.name == 'dir' && !element.attributes['deleted']
2157 #- sub-album image/caption
2158 add_subalbum.call(element, subalbums_counter += 1)
2159 total[element.name] += 1
2162 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2163 total['image'], total['video'], total['dir'] ]))
2164 $subalbums_vb.add($subalbums)
2165 $subalbums_vb.show_all
2167 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2168 $notebook.get_tab_label($autotable_sw).sensitive = false
2169 $notebook.set_page(0)
2170 $thumbnails_title.buffer.text = ''
2172 $notebook.get_tab_label($autotable_sw).sensitive = true
2173 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2176 if !$xmldir.child_byname_notattr('dir', 'deleted')
2177 $notebook.get_tab_label($subalbums_sw).sensitive = false
2178 $notebook.set_page(1)
2180 $notebook.get_tab_label($subalbums_sw).sensitive = true
2184 def pixbuf_or_nil(filename)
2186 return Gdk::Pixbuf.new(filename)
2192 def theme_choose(current)
2193 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2195 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2196 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2197 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2199 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2200 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2201 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2202 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2203 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2204 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2205 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2206 treeview.signal_connect('button-press-event') { |w, event|
2207 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2208 dialog.response(Gtk::Dialog::RESPONSE_OK)
2212 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2214 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2217 iter[0] = File.basename(dir)
2218 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2219 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2220 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2221 if File.basename(dir) == current
2222 treeview.selection.select_iter(iter)
2226 dialog.set_default_size(700, 400)
2227 dialog.vbox.show_all
2228 dialog.run { |response|
2229 iter = treeview.selection.selected
2231 if response == Gtk::Dialog::RESPONSE_OK && iter
2232 return model.get_value(iter, 0)
2238 def show_password_protections
2239 examine_dir_elem = Proc.new { |parent_iter, xmldir, already_protected|
2240 child_iter = $albums_iters[xmldir.attributes['path']]
2241 if xmldir.attributes['password-protect']
2242 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2243 already_protected = true
2244 elsif already_protected
2245 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2247 pix = pix.saturate_and_pixelate(1, true)
2253 xmldir.elements.each('dir') { |elem|
2254 if !elem.attributes['deleted']
2255 examine_dir_elem.call(child_iter, elem, already_protected)
2259 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2262 def populate_subalbums_treeview
2266 $subalbums_vb.children.each { |chld|
2267 $subalbums_vb.remove(chld)
2270 source = $xmldoc.root.attributes['source']
2271 msg 3, "source: #{source}"
2273 xmldir = $xmldoc.elements['//dir']
2274 if !xmldir || xmldir.attributes['path'] != source
2275 msg 1, _("Corrupted booh file...")
2279 append_dir_elem = Proc.new { |parent_iter, xmldir|
2280 child_iter = $albums_ts.append(parent_iter)
2281 child_iter[0] = File.basename(xmldir.attributes['path'])
2282 child_iter[1] = xmldir.attributes['path']
2283 $albums_iters[xmldir.attributes['path']] = child_iter
2284 msg 3, "puttin location: #{xmldir.attributes['path']}"
2285 xmldir.elements.each('dir') { |elem|
2286 if !elem.attributes['deleted']
2287 append_dir_elem.call(child_iter, elem)
2291 append_dir_elem.call(nil, xmldir)
2292 show_password_protections
2294 $albums_tv.expand_all
2295 $albums_tv.selection.select_iter($albums_ts.iter_first)
2298 def open_file(filename)
2302 $current_path = nil #- invalidate
2303 $modified_pixbufs = {}
2306 $subalbums_vb.children.each { |chld|
2307 $subalbums_vb.remove(chld)
2310 if !File.exists?(filename)
2311 return utf8(_("File not found."))
2315 $xmldoc = REXML::Document.new File.new(filename)
2320 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2321 if entry2type(filename).nil?
2322 return utf8(_("Not a booh file!"))
2324 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."))
2328 if !source = $xmldoc.root.attributes['source']
2329 return utf8(_("Corrupted booh file..."))
2332 if !dest = $xmldoc.root.attributes['destination']
2333 return utf8(_("Corrupted booh file..."))
2336 if !theme = $xmldoc.root.attributes['theme']
2337 return utf8(_("Corrupted booh file..."))
2340 if $xmldoc.root.attributes['version'] != $VERSION
2341 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2342 mark_document_as_dirty
2343 $xmldoc.root.add_attribute('version', $VERSION)
2346 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2347 optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2348 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2350 $filename = filename
2351 select_theme(theme, limit_sizes, optimizefor32, nperrow)
2352 $default_size['thumbnails'] =~ /(.*)x(.*)/
2353 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2354 $albums_thumbnail_size =~ /(.*)x(.*)/
2355 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2357 populate_subalbums_treeview
2359 $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
2363 def open_file_user(filename)
2364 result = open_file(filename)
2366 $config['last-opens'] ||= []
2367 if $config['last-opens'][-1] != utf8(filename)
2368 $config['last-opens'] << utf8(filename)
2370 $orig_filename = $filename
2371 tmp = Tempfile.new("boohtemp")
2374 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2376 $tempfiles << $filename << "#{$filename}.backup"
2378 $orig_filename = nil
2384 if !ask_save_modifications(utf8(_("Save this album?")),
2385 utf8(_("Do you want to save the changes to this album?")),
2386 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2389 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2391 Gtk::FileChooser::ACTION_OPEN,
2393 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2394 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2395 fc.set_current_folder(File.expand_path("~/.booh"))
2396 fc.transient_for = $main_window
2399 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2400 push_mousecursor_wait(fc)
2401 msg = open_file_user(fc.filename)
2417 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2422 def additional_booh_options
2425 options += "--mproc #{$config['mproc'].to_i} "
2427 if $config['emptycomments']
2428 options += "--empty-comments "
2434 if !ask_save_modifications(utf8(_("Save this album?")),
2435 utf8(_("Do you want to save the changes to this album?")),
2436 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2439 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2441 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2442 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2443 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2445 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2446 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2447 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2448 tbl.attach(src = Gtk::Entry.new,
2449 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2450 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2451 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2452 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2453 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2454 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2455 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2456 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2457 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2458 tbl.attach(dest = Gtk::Entry.new,
2459 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2460 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2461 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2462 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2463 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2464 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2465 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2466 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2467 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2469 tooltips = Gtk::Tooltips.new
2470 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2471 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2472 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2473 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2474 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2475 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2476 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)
2477 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2478 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2479 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2480 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2481 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)
2483 src_nb_calculated_for = ''
2485 process_src_nb = Proc.new {
2486 if src.text != src_nb_calculated_for
2487 src_nb_calculated_for = src.text
2489 Thread.kill(src_nb_thread)
2492 if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2493 if File.readable?(from_utf8(src_nb_calculated_for))
2494 src_nb_thread = Thread.new {
2495 gtk_thread_protect { puts "destroyed: " + src_nb.destroyed?.to_s; src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2496 total = { 'image' => 0, 'video' => 0, nil => 0 }
2497 `find '#{from_utf8(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2498 if File.basename(dir) =~ /^\./
2502 Dir.entries(dir.chomp).each { |file|
2503 total[entry2type(file)] += 1
2505 rescue Errno::EACCES, Errno::ENOENT
2509 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'] ])) }
2513 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2516 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2521 timeout_src_nb = Gtk.timeout_add(100) {
2525 src_browse.signal_connect('clicked') {
2526 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2528 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2530 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2531 fc.transient_for = $main_window
2532 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2533 src.text = utf8(fc.filename)
2535 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2540 dest_browse.signal_connect('clicked') {
2541 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2543 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2545 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2546 fc.transient_for = $main_window
2547 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2548 dest.text = utf8(fc.filename)
2553 conf_browse.signal_connect('clicked') {
2554 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2556 Gtk::FileChooser::ACTION_SAVE,
2558 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2559 fc.transient_for = $main_window
2560 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2561 fc.set_current_folder(File.expand_path("~/.booh"))
2562 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2563 conf.text = utf8(fc.filename)
2570 recreate_theme_config = proc {
2571 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2573 select_theme(theme_button.label, 'all', optimize432.active?, nil)
2574 $images_size.each { |s|
2575 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2579 tooltips.set_tip(cb, utf8(s['description']), nil)
2580 theme_sizes << { :widget => cb, :value => s['name'] }
2582 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2583 tooltips = Gtk::Tooltips.new
2584 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2585 theme_sizes << { :widget => cb, :value => 'original' }
2588 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2591 $allowed_N_values.each { |n|
2593 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2595 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2597 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2601 nperrows << { :widget => rb, :value => n }
2603 nperrowradios.show_all
2605 recreate_theme_config.call
2607 theme_button.signal_connect('clicked') {
2608 if newtheme = theme_choose(theme_button.label)
2609 theme_button.label = newtheme
2610 recreate_theme_config.call
2614 dialog.vbox.add(frame1)
2615 dialog.vbox.add(frame2)
2616 dialog.window_position = Gtk::Window::POS_MOUSE
2622 dialog.run { |response|
2623 if response == Gtk::Dialog::RESPONSE_OK
2624 srcdir = from_utf8(src.text)
2625 destdir = from_utf8(dest.text)
2626 if !File.directory?(srcdir)
2627 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2629 elsif conf.text == ''
2630 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2632 elsif File.directory?(from_utf8(conf.text))
2633 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
2635 elsif destdir != make_dest_filename(destdir)
2636 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2638 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
2639 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2641 elsif File.exists?(destdir) && !File.directory?(destdir)
2642 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2644 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2645 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2647 system("mkdir '#{destdir}'")
2648 if !File.directory?(destdir)
2649 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2660 srcdir = from_utf8(src.text)
2661 destdir = from_utf8(dest.text)
2662 configskel = File.expand_path(from_utf8(conf.text))
2663 theme = theme_button.label
2664 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2665 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2666 opt432 = optimize432.active?
2667 madewith = madewithentry.text
2669 Thread.kill(src_nb_thread)
2670 gtk_thread_abandon #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2673 Gtk.timeout_remove(timeout_src_nb)
2676 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2677 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2678 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' #{additional_booh_options}",
2679 utf8(_("Please wait while scanning source directory...")),
2681 { :closure_after => proc { open_file_user(configskel) } })
2686 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2688 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2689 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2690 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2692 source = $xmldoc.root.attributes['source']
2693 dest = $xmldoc.root.attributes['destination']
2694 theme = $xmldoc.root.attributes['theme']
2695 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2696 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2697 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2699 limit_sizes = limit_sizes.split(/,/)
2701 madewith = $xmldoc.root.attributes['made-with']
2703 tooltips = Gtk::Tooltips.new
2704 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2705 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2706 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2707 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2708 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2709 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2710 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2711 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
2712 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2713 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2714 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2715 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
2716 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2718 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2719 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2720 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2721 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2722 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2723 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2724 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)
2725 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2726 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2727 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2728 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2730 madewithentry.text = madewith
2732 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)
2736 recreate_theme_config = proc {
2737 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2739 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
2741 $images_size.each { |s|
2742 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2744 if limit_sizes.include?(s['name'])
2752 tooltips.set_tip(cb, utf8(s['description']), nil)
2753 theme_sizes << { :widget => cb, :value => s['name'] }
2755 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2756 tooltips = Gtk::Tooltips.new
2757 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2758 if limit_sizes && limit_sizes.include?('original')
2761 theme_sizes << { :widget => cb, :value => 'original' }
2764 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2767 $allowed_N_values.each { |n|
2769 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2771 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2773 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2774 nperrowradios.add(Gtk::Label.new(' '))
2775 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
2778 nperrows << { :widget => rb, :value => n.to_s }
2780 nperrowradios.show_all
2782 recreate_theme_config.call
2784 theme_button.signal_connect('clicked') {
2785 if newtheme = theme_choose(theme_button.label)
2788 theme_button.label = newtheme
2789 recreate_theme_config.call
2793 dialog.vbox.add(frame1)
2794 dialog.vbox.add(frame2)
2795 dialog.window_position = Gtk::Window::POS_MOUSE
2801 dialog.run { |response|
2802 if response == Gtk::Dialog::RESPONSE_OK
2803 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2804 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2813 save_theme = theme_button.label
2814 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
2815 save_opt432 = optimize432.active?
2816 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2817 save_madewith = madewithentry.text
2820 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith)
2821 mark_document_as_dirty
2823 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
2824 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
2825 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' #{additional_booh_options}",
2826 utf8(_("Please wait while scanning source directory...")),
2828 { :closure_after => proc {
2829 open_file($filename)
2838 sel = $albums_tv.selection.selected_rows
2840 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2841 "--verbose-level #{$verbose_level} #{additional_booh_options}",
2842 utf8(_("Please wait while scanning source directory...")),
2844 { :closure_after => proc {
2845 open_file($filename)
2846 $albums_tv.selection.select_path(sel[0])
2854 sel = $albums_tv.selection.selected_rows
2856 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2857 "--verbose-level #{$verbose_level} #{additional_booh_options}",
2858 utf8(_("Please wait while scanning source directory...")),
2860 { :closure_after => proc {
2861 open_file($filename)
2862 $albums_tv.selection.select_path(sel[0])
2870 theme = $xmldoc.root.attributes['theme']
2871 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2873 limit_sizes = "--sizes #{limit_sizes}"
2875 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
2876 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
2877 utf8(_("Please wait while scanning source directory...")),
2879 { :closure_after => proc {
2880 open_file($filename)
2886 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
2888 Gtk::FileChooser::ACTION_SAVE,
2890 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2891 fc.transient_for = $main_window
2892 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2893 fc.set_current_folder(File.expand_path("~/.booh"))
2894 fc.filename = $orig_filename
2895 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2896 $orig_filename = fc.filename
2897 save_current_file_user
2903 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
2905 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2906 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2907 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2909 dialog.vbox.add(notebook = Gtk::Notebook.new)
2910 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
2911 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
2912 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2913 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
2914 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2915 tooltips = Gtk::Tooltips.new
2916 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
2917 for example: /usr/bin/mplayer %f")), nil)
2918 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
2919 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2920 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
2921 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2922 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
2923 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
2924 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
2925 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2926 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)),
2927 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2928 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)
2929 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
2930 0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2931 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)
2932 tbl.attach(emptycomments_check = Gtk::CheckButton.new(utf8(_("Use empty comments for new albums"))),
2933 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
2934 tooltips.set_tip(emptycomments_check, utf8(_("Normally, filenames are used as comments for new albums. Check this if you prefer empty comments.")), nil)
2935 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
2936 0, 2, 5, 6, Gtk::FILL, Gtk::SHRINK, 2, 2)
2937 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)
2938 smp_check.signal_connect('toggled') {
2939 if smp_check.active?
2940 smp_hbox.sensitive = true
2942 smp_hbox.sensitive = false
2946 smp_check.active = true
2947 smp_spin.value = $config['mproc'].to_i
2949 nogestures_check.active = $config['nogestures']
2950 emptycomments_check.active = $config['emptycomments']
2951 deleteondisk_check.active = $config['deleteondisk']
2953 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
2954 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
2955 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2956 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
2957 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2959 dialog.vbox.show_all
2960 dialog.run { |response|
2961 if response == Gtk::Dialog::RESPONSE_OK
2962 $config['video-viewer'] = video_viewer_entry.text
2963 $config['browser'] = browser_entry.text
2964 if smp_check.active?
2965 $config['mproc'] = smp_spin.value.to_i
2967 $config.delete('mproc')
2969 $config['nogestures'] = nogestures_check.active?
2970 $config['emptycomments'] = emptycomments_check.active?
2971 $config['deleteondisk'] = deleteondisk_check.active?
2973 $config['convert-enhance'] = enhance_entry.text
2980 if $undo_tb.sensitive?
2981 $redo_tb.sensitive = $redo_mb.sensitive = true
2982 if not more_undoes = UndoHandler.undo($statusbar)
2983 $undo_tb.sensitive = $undo_mb.sensitive = false
2989 if $redo_tb.sensitive?
2990 $undo_tb.sensitive = $undo_mb.sensitive = true
2991 if not more_redoes = UndoHandler.redo($statusbar)
2992 $redo_tb.sensitive = $redo_mb.sensitive = false
2997 def show_one_click_explanation(intro)
2998 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
3000 %s When such a tool is activated
3001 (<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
3002 on a thumbnail will immediately apply the desired action.
3004 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
3010 GNU GENERAL PUBLIC LICENSE
3011 Version 2, June 1991
3013 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
3014 675 Mass Ave, Cambridge, MA 02139, USA
3015 Everyone is permitted to copy and distribute verbatim copies
3016 of this license document, but changing it is not allowed.
3020 The licenses for most software are designed to take away your
3021 freedom to share and change it. By contrast, the GNU General Public
3022 License is intended to guarantee your freedom to share and change free
3023 software--to make sure the software is free for all its users. This
3024 General Public License applies to most of the Free Software
3025 Foundation's software and to any other program whose authors commit to
3026 using it. (Some other Free Software Foundation software is covered by
3027 the GNU Library General Public License instead.) You can apply it to
3030 When we speak of free software, we are referring to freedom, not
3031 price. Our General Public Licenses are designed to make sure that you
3032 have the freedom to distribute copies of free software (and charge for
3033 this service if you wish), that you receive source code or can get it
3034 if you want it, that you can change the software or use pieces of it
3035 in new free programs; and that you know you can do these things.
3037 To protect your rights, we need to make restrictions that forbid
3038 anyone to deny you these rights or to ask you to surrender the rights.
3039 These restrictions translate to certain responsibilities for you if you
3040 distribute copies of the software, or if you modify it.
3042 For example, if you distribute copies of such a program, whether
3043 gratis or for a fee, you must give the recipients all the rights that
3044 you have. You must make sure that they, too, receive or can get the
3045 source code. And you must show them these terms so they know their
3048 We protect your rights with two steps: (1) copyright the software, and
3049 (2) offer you this license which gives you legal permission to copy,
3050 distribute and/or modify the software.
3052 Also, for each author's protection and ours, we want to make certain
3053 that everyone understands that there is no warranty for this free
3054 software. If the software is modified by someone else and passed on, we
3055 want its recipients to know that what they have is not the original, so
3056 that any problems introduced by others will not reflect on the original
3057 authors' reputations.
3059 Finally, any free program is threatened constantly by software
3060 patents. We wish to avoid the danger that redistributors of a free
3061 program will individually obtain patent licenses, in effect making the
3062 program proprietary. To prevent this, we have made it clear that any
3063 patent must be licensed for everyone's free use or not licensed at all.
3065 The precise terms and conditions for copying, distribution and
3066 modification follow.
3069 GNU GENERAL PUBLIC LICENSE
3070 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
3072 0. This License applies to any program or other work which contains
3073 a notice placed by the copyright holder saying it may be distributed
3074 under the terms of this General Public License. The "Program", below,
3075 refers to any such program or work, and a "work based on the Program"
3076 means either the Program or any derivative work under copyright law:
3077 that is to say, a work containing the Program or a portion of it,
3078 either verbatim or with modifications and/or translated into another
3079 language. (Hereinafter, translation is included without limitation in
3080 the term "modification".) Each licensee is addressed as "you".
3082 Activities other than copying, distribution and modification are not
3083 covered by this License; they are outside its scope. The act of
3084 running the Program is not restricted, and the output from the Program
3085 is covered only if its contents constitute a work based on the
3086 Program (independent of having been made by running the Program).
3087 Whether that is true depends on what the Program does.
3089 1. You may copy and distribute verbatim copies of the Program's
3090 source code as you receive it, in any medium, provided that you
3091 conspicuously and appropriately publish on each copy an appropriate
3092 copyright notice and disclaimer of warranty; keep intact all the
3093 notices that refer to this License and to the absence of any warranty;
3094 and give any other recipients of the Program a copy of this License
3095 along with the Program.
3097 You may charge a fee for the physical act of transferring a copy, and
3098 you may at your option offer warranty protection in exchange for a fee.
3100 2. You may modify your copy or copies of the Program or any portion
3101 of it, thus forming a work based on the Program, and copy and
3102 distribute such modifications or work under the terms of Section 1
3103 above, provided that you also meet all of these conditions:
3105 a) You must cause the modified files to carry prominent notices
3106 stating that you changed the files and the date of any change.
3108 b) You must cause any work that you distribute or publish, that in
3109 whole or in part contains or is derived from the Program or any
3110 part thereof, to be licensed as a whole at no charge to all third
3111 parties under the terms of this License.
3113 c) If the modified program normally reads commands interactively
3114 when run, you must cause it, when started running for such
3115 interactive use in the most ordinary way, to print or display an
3116 announcement including an appropriate copyright notice and a
3117 notice that there is no warranty (or else, saying that you provide
3118 a warranty) and that users may redistribute the program under
3119 these conditions, and telling the user how to view a copy of this
3120 License. (Exception: if the Program itself is interactive but
3121 does not normally print such an announcement, your work based on
3122 the Program is not required to print an announcement.)
3125 These requirements apply to the modified work as a whole. If
3126 identifiable sections of that work are not derived from the Program,
3127 and can be reasonably considered independent and separate works in
3128 themselves, then this License, and its terms, do not apply to those
3129 sections when you distribute them as separate works. But when you
3130 distribute the same sections as part of a whole which is a work based
3131 on the Program, the distribution of the whole must be on the terms of
3132 this License, whose permissions for other licensees extend to the
3133 entire whole, and thus to each and every part regardless of who wrote it.
3135 Thus, it is not the intent of this section to claim rights or contest
3136 your rights to work written entirely by you; rather, the intent is to
3137 exercise the right to control the distribution of derivative or
3138 collective works based on the Program.
3140 In addition, mere aggregation of another work not based on the Program
3141 with the Program (or with a work based on the Program) on a volume of
3142 a storage or distribution medium does not bring the other work under
3143 the scope of this License.
3145 3. You may copy and distribute the Program (or a work based on it,
3146 under Section 2) in object code or executable form under the terms of
3147 Sections 1 and 2 above provided that you also do one of the following:
3149 a) Accompany it with the complete corresponding machine-readable
3150 source code, which must be distributed under the terms of Sections
3151 1 and 2 above on a medium customarily used for software interchange; or,
3153 b) Accompany it with a written offer, valid for at least three
3154 years, to give any third party, for a charge no more than your
3155 cost of physically performing source distribution, a complete
3156 machine-readable copy of the corresponding source code, to be
3157 distributed under the terms of Sections 1 and 2 above on a medium
3158 customarily used for software interchange; or,
3160 c) Accompany it with the information you received as to the offer
3161 to distribute corresponding source code. (This alternative is
3162 allowed only for noncommercial distribution and only if you
3163 received the program in object code or executable form with such
3164 an offer, in accord with Subsection b above.)
3166 The source code for a work means the preferred form of the work for
3167 making modifications to it. For an executable work, complete source
3168 code means all the source code for all modules it contains, plus any
3169 associated interface definition files, plus the scripts used to
3170 control compilation and installation of the executable. However, as a
3171 special exception, the source code distributed need not include
3172 anything that is normally distributed (in either source or binary
3173 form) with the major components (compiler, kernel, and so on) of the
3174 operating system on which the executable runs, unless that component
3175 itself accompanies the executable.
3177 If distribution of executable or object code is made by offering
3178 access to copy from a designated place, then offering equivalent
3179 access to copy the source code from the same place counts as
3180 distribution of the source code, even though third parties are not
3181 compelled to copy the source along with the object code.
3184 4. You may not copy, modify, sublicense, or distribute the Program
3185 except as expressly provided under this License. Any attempt
3186 otherwise to copy, modify, sublicense or distribute the Program is
3187 void, and will automatically terminate your rights under this License.
3188 However, parties who have received copies, or rights, from you under
3189 this License will not have their licenses terminated so long as such
3190 parties remain in full compliance.
3192 5. You are not required to accept this License, since you have not
3193 signed it. However, nothing else grants you permission to modify or
3194 distribute the Program or its derivative works. These actions are
3195 prohibited by law if you do not accept this License. Therefore, by
3196 modifying or distributing the Program (or any work based on the
3197 Program), you indicate your acceptance of this License to do so, and
3198 all its terms and conditions for copying, distributing or modifying
3199 the Program or works based on it.
3201 6. Each time you redistribute the Program (or any work based on the
3202 Program), the recipient automatically receives a license from the
3203 original licensor to copy, distribute or modify the Program subject to
3204 these terms and conditions. You may not impose any further
3205 restrictions on the recipients' exercise of the rights granted herein.
3206 You are not responsible for enforcing compliance by third parties to
3209 7. If, as a consequence of a court judgment or allegation of patent
3210 infringement or for any other reason (not limited to patent issues),
3211 conditions are imposed on you (whether by court order, agreement or
3212 otherwise) that contradict the conditions of this License, they do not
3213 excuse you from the conditions of this License. If you cannot
3214 distribute so as to satisfy simultaneously your obligations under this
3215 License and any other pertinent obligations, then as a consequence you
3216 may not distribute the Program at all. For example, if a patent
3217 license would not permit royalty-free redistribution of the Program by
3218 all those who receive copies directly or indirectly through you, then
3219 the only way you could satisfy both it and this License would be to
3220 refrain entirely from distribution of the Program.
3222 If any portion of this section is held invalid or unenforceable under
3223 any particular circumstance, the balance of the section is intended to
3224 apply and the section as a whole is intended to apply in other
3227 It is not the purpose of this section to induce you to infringe any
3228 patents or other property right claims or to contest validity of any
3229 such claims; this section has the sole purpose of protecting the
3230 integrity of the free software distribution system, which is
3231 implemented by public license practices. Many people have made
3232 generous contributions to the wide range of software distributed
3233 through that system in reliance on consistent application of that
3234 system; it is up to the author/donor to decide if he or she is willing
3235 to distribute software through any other system and a licensee cannot
3238 This section is intended to make thoroughly clear what is believed to
3239 be a consequence of the rest of this License.
3242 8. If the distribution and/or use of the Program is restricted in
3243 certain countries either by patents or by copyrighted interfaces, the
3244 original copyright holder who places the Program under this License
3245 may add an explicit geographical distribution limitation excluding
3246 those countries, so that distribution is permitted only in or among
3247 countries not thus excluded. In such case, this License incorporates
3248 the limitation as if written in the body of this License.
3250 9. The Free Software Foundation may publish revised and/or new versions
3251 of the General Public License from time to time. Such new versions will
3252 be similar in spirit to the present version, but may differ in detail to
3253 address new problems or concerns.
3255 Each version is given a distinguishing version number. If the Program
3256 specifies a version number of this License which applies to it and "any
3257 later version", you have the option of following the terms and conditions
3258 either of that version or of any later version published by the Free
3259 Software Foundation. If the Program does not specify a version number of
3260 this License, you may choose any version ever published by the Free Software
3263 10. If you wish to incorporate parts of the Program into other free
3264 programs whose distribution conditions are different, write to the author
3265 to ask for permission. For software which is copyrighted by the Free
3266 Software Foundation, write to the Free Software Foundation; we sometimes
3267 make exceptions for this. Our decision will be guided by the two goals
3268 of preserving the free status of all derivatives of our free software and
3269 of promoting the sharing and reuse of software generally.
3273 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
3274 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
3275 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
3276 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
3277 OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
3278 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
3279 TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
3280 PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
3281 REPAIR OR CORRECTION.
3283 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
3284 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
3285 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
3286 INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
3287 OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
3288 TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
3289 YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
3290 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
3291 POSSIBILITY OF SUCH DAMAGES.
3295 def create_menu_and_toolbar
3298 mb = Gtk::MenuBar.new
3300 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
3301 filesubmenu = Gtk::Menu.new
3302 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
3303 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
3304 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3305 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
3306 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
3307 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3308 tooltips = Gtk::Tooltips.new
3309 filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
3310 $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3311 tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
3312 filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
3313 $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3314 tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
3315 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
3316 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3317 tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums (anywhere)")), nil)
3318 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3319 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
3320 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3321 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
3322 filesubmenu.append($view_wa = Gtk::ImageMenuItem.new(utf8(_("View web-album with browser"))).set_sensitive(false))
3323 $view_wa.image = Gtk::Image.new("#{$FPATH}/images/stock-view-webalbum-16.png")
3324 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3325 filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
3326 tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
3327 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3328 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
3329 filemenu.set_submenu(filesubmenu)
3332 new.signal_connect('activate') { new_album }
3333 open.signal_connect('activate') { open_file_popup }
3334 $save.signal_connect('activate') { save_current_file_user }
3335 $save_as.signal_connect('activate') { save_as_do }
3336 $merge_current.signal_connect('activate') { merge_current }
3337 $merge_newsubs.signal_connect('activate') { merge_newsubs }
3338 $merge.signal_connect('activate') { merge }
3339 $generate.signal_connect('activate') {
3341 call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
3342 utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
3344 { :successmsg => utf8(_("Your web-album is now ready in directory '%s'.
3345 Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
3346 :successmsg_linkurl => $xmldoc.root.attributes['destination'],
3347 :closure_after => proc {
3348 $xmldoc.elements.each('//dir') { |elem|
3349 elem.add_attribute('already-generated', 'true')
3351 UndoHandler.cleanup #- prevent save_changes to mark current dir as not already generated
3352 $undo_tb.sensitive = $undo_mb.sensitive = false
3353 $redo_tb.sensitive = $redo_mb.sensitive = false
3355 $generated_outofline = true
3358 $view_wa.signal_connect('activate') {
3359 indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
3360 if File.exists?(indexhtml)
3363 show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
3366 $properties.signal_connect('activate') { properties }
3368 quit.signal_connect('activate') { try_quit }
3370 editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
3371 editsubmenu = Gtk::Menu.new
3372 editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
3373 editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
3374 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3375 editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
3376 $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
3377 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)
3378 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3379 editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
3380 editmenu.set_submenu(editsubmenu)
3383 $remove_all_captions.signal_connect('activate') { remove_all_captions }
3385 prefs.signal_connect('activate') { preferences }
3387 helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
3388 helpsubmenu = Gtk::Menu.new
3389 helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
3390 one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
3391 helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
3392 speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
3393 helpsubmenu.append(tutos = Gtk::ImageMenuItem.new(utf8(_("Online tutorials (opens a web-browser)"))))
3394 tutos.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3395 helpsubmenu.append( Gtk::SeparatorMenuItem.new)
3396 helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
3397 helpmenu.set_submenu(helpsubmenu)
3400 one_click.signal_connect('activate') {
3401 show_one_click_explanation(_("One-Click tools are available in the toolbar."))
3404 speed.signal_connect('activate') {
3405 show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
3407 <span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
3408 <span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
3409 <span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
3410 <span foreground='darkblue'>Control-Enter</span>: for an image, open larger view; for a video, launch player
3411 <span foreground='darkblue'>Control-Delete</span>: delete image
3412 <span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
3413 <span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
3414 <span foreground='darkblue'>Control-z</span>: undo
3415 <span foreground='darkblue'>Control-r</span>: redo
3417 <span size='large' weight='bold'>Mouse gestures:</span>
3419 Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
3420 for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
3422 <span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
3423 <span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
3424 <span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
3425 <span foreground='darkblue'>Left click, hold left button, right click</span>: undo
3426 <span foreground='darkblue'>Right click, hold right button, left click</span>: redo
3427 ")), { :pos_centered => true, :not_transient => true })
3430 tutos.signal_connect('activate') {
3431 open_url('http://zarb.org/~gc/html/booh/tutorial.html')
3434 about.signal_connect('activate') {
3435 Gtk::AboutDialog.set_url_hook { |dialog, url| open_url(url) }
3436 Gtk::AboutDialog.show($main_window, { :name => 'booh',
3437 :version => $VERSION,
3438 :copyright => 'Copyright (c) 2005 Guillaume Cottenceau',
3439 :license => get_license,
3440 :website => 'http://zarb.org/~gc/html/booh.html',
3441 :authors => [ 'Guillaume Cottenceau' ],
3442 :artists => [ 'Ayo73' ],
3443 :comments => utf8(_("''The Web-Album of choice for discriminating Linux users''")),
3444 :translator_credits => utf8(_('Japanese: Masao Mutoh
3445 German: Roland Eckert
3446 French: Guillaume Cottenceau')),
3447 :logo => Gdk::Pixbuf.new("#{$FPATH}/images/logo.png") })
3452 tb = Gtk::Toolbar.new
3454 tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
3455 open.label = utf8(_("Open")) #- to avoid missing gtk2 l10n catalogs
3456 open.menu = Gtk::Menu.new
3457 open.signal_connect('clicked') { open_file_popup }
3458 open.signal_connect('show-menu') {
3459 lastopens = Gtk::Menu.new
3461 if $config['last-opens']
3462 $config['last-opens'].reverse.each { |e|
3463 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
3464 item.signal_connect('activate') {
3465 if ask_save_modifications(utf8(_("Save this album?")),
3466 utf8(_("Do you want to save the changes to this album?")),
3467 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3468 push_mousecursor_wait
3469 msg = open_file_user(from_utf8(e))
3472 show_popup($main_window, msg)
3480 open.menu = lastopens
3483 tb.insert(-1, Gtk::SeparatorToolItem.new)
3485 tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
3486 $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
3487 $r90.label = utf8(_("Rotate"))
3488 tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
3489 $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
3490 $r270.label = utf8(_("Rotate"))
3491 tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
3492 $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
3493 $enhance.label = utf8(_("Enhance"))
3494 tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
3495 $delete.label = utf8(_("Delete")) #- to avoid missing gtk2 l10n catalogs
3496 tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
3497 nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
3498 nothing.label = utf8(_("None"))
3500 tb.insert(-1, Gtk::SeparatorToolItem.new)
3502 tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
3503 tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
3506 $undo_tb.signal_connect('clicked') { perform_undo }
3507 $undo_mb.signal_connect('activate') { perform_undo }