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