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 > 10
155 $config['last-opens'] = $config['last-opens'][-10, 10]
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 && $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)
704 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
710 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
711 distribute_multiple_call = proc { |action, arg|
712 $selected_elements.each_key { |path|
713 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
715 if possible_actions[:can_multiple] && $selected_elements.length > 0
716 UndoHandler.begin_batch
717 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
718 UndoHandler.end_batch
720 closures[action].call(arg)
722 $selected_elements = {}
725 if optionals.include?('change_image')
726 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
727 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
728 changeimg.signal_connect('activate') { closures[:change].call }
729 menu.append(Gtk::SeparatorMenuItem.new)
731 if !possible_actions[:can_multiple] || $selected_elements.length == 0
734 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
735 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
736 view.signal_connect('activate') { closures[:view].call }
738 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
739 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
740 view.signal_connect('activate') { closures[:view].call }
741 menu.append(Gtk::SeparatorMenuItem.new)
744 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
745 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
746 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
747 exif.signal_connect('activate') { show_popup($main_window,
748 utf8(`identify -format "%[EXIF:*]" #{fullpath}`.sub(/MakerNote.*\n/, '')),
749 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
750 menu.append(Gtk::SeparatorMenuItem.new)
753 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
754 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
755 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
756 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
757 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
758 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
759 if !possible_actions[:can_multiple] || $selected_elements.length == 0
760 menu.append(Gtk::SeparatorMenuItem.new)
761 if !possible_actions[:forbid_left]
762 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
763 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
764 moveleft.signal_connect('activate') { closures[:move].call('left') }
765 if !possible_actions[:can_left]
766 moveleft.sensitive = false
769 if !possible_actions[:forbid_right]
770 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
771 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
772 moveright.signal_connect('activate') { closures[:move].call('right') }
773 if !possible_actions[:can_right]
774 moveright.sensitive = false
777 if optionals.include?('move_top')
778 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
779 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
780 movetop.signal_connect('activate') { closures[:move].call('top') }
781 if !possible_actions[:can_top]
782 movetop.sensitive = false
785 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
786 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
787 moveup.signal_connect('activate') { closures[:move].call('up') }
788 if !possible_actions[:can_up]
789 moveup.sensitive = false
791 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
792 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
793 movedown.signal_connect('activate') { closures[:move].call('down') }
794 if !possible_actions[:can_down]
795 movedown.sensitive = false
797 if optionals.include?('move_bottom')
798 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
799 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
800 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
801 if !possible_actions[:can_bottom]
802 movebottom.sensitive = false
807 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
808 menu.append(Gtk::SeparatorMenuItem.new)
809 menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
810 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
811 color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
812 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
813 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
814 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
815 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
816 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
817 frame_offset.signal_connect('activate') {
818 if possible_actions[:can_multiple] && $selected_elements.length > 0
819 if values = ask_new_frame_offset(nil, '')
820 distribute_multiple_call.call(:frame_offset, values)
823 closures[:frame_offset].call
828 menu.append( Gtk::SeparatorMenuItem.new)
829 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
830 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
831 whitebalance.signal_connect('activate') {
832 if possible_actions[:can_multiple] && $selected_elements.length > 0
833 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
834 distribute_multiple_call.call(:whitebalance, values)
837 closures[:whitebalance].call
840 if !possible_actions[:can_multiple] || $selected_elements.length == 0
841 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
842 _("Enhance constrast"))))
844 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
846 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
847 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
848 if type == 'image' && possible_actions[:can_panorama]
849 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
850 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
851 panorama.signal_connect('activate') {
852 if possible_actions[:can_multiple] && $selected_elements.length > 0
853 if values = ask_new_pano_amount(nil, '')
854 distribute_multiple_call.call(:pano, values)
857 distribute_multiple_call.call(:pano)
861 if optionals.include?('delete')
862 menu.append( Gtk::SeparatorMenuItem.new)
863 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
864 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
865 if !possible_actions[:can_multiple] || $selected_elements.length == 0
866 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
867 paste_item.signal_connect('activate') { closures[:paste].call }
868 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
869 clear_item.signal_connect('activate') { $cuts = [] }
871 paste_item.sensitive = clear_item.sensitive = false
874 menu.append( Gtk::SeparatorMenuItem.new)
875 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
876 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
879 menu.popup(nil, nil, event.button, event.time)
882 def delete_current_subalbum
884 sel = $albums_tv.selection.selected_rows
885 $xmldir.elements.each { |e|
886 if e.name == 'image' || e.name == 'video'
887 e.add_attribute('deleted', 'true')
890 #- branch if we have a non deleted subalbum
891 if $xmldir.child_byname_notattr('dir', 'deleted')
892 $xmldir.delete_attribute('thumbnails-caption')
893 $xmldir.delete_attribute('thumbnails-captionfile')
895 $xmldir.add_attribute('deleted', 'true')
897 while moveup.parent.name == 'dir'
898 moveup = moveup.parent
899 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
900 moveup.add_attribute('deleted', 'true')
907 save_changes('forced')
908 populate_subalbums_treeview(false)
909 $albums_tv.selection.select_path(sel[0])
915 $current_path = nil #- prevent save_changes from being rerun again
916 sel = $albums_tv.selection.selected_rows
917 restore_one = proc { |xmldir|
918 xmldir.elements.each { |e|
919 if e.name == 'dir' && e.attributes['deleted']
922 e.delete_attribute('deleted')
925 restore_one.call($xmldir)
926 populate_subalbums_treeview(false)
927 $albums_tv.selection.select_path(sel[0])
930 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
933 frame1 = Gtk::Frame.new
934 fullpath = from_utf8("#{$current_path}/#{filename}")
936 my_gen_real_thumbnail = proc {
937 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
940 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
941 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
942 frame1.add(img = Gtk::Image.new)
943 my_gen_real_thumbnail.call
945 frame1.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
947 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
949 tooltips = Gtk::Tooltips.new
950 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
951 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
953 frame2, textview = create_editzone($autotable_sw, 1, img)
954 textview.buffer.text = caption
955 textview.set_justification(Gtk::Justification::CENTER)
957 vbox = Gtk::VBox.new(false, 5)
958 vbox.pack_start(evtbox, false, false)
959 vbox.pack_start(frame2, false, false)
960 autotable.append(vbox, filename)
962 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
963 $vbox2widgets[vbox] = { :textview => textview, :image => img }
965 #- to be able to find widgets by name
966 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
968 cleanup_all_thumbnails = proc {
969 #- remove out of sync images
970 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
971 for sizeobj in $images_size
972 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
977 rotate_and_cleanup = proc { |angle|
978 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
979 cleanup_all_thumbnails.call
982 move = proc { |direction|
983 do_method = "move_#{direction}"
984 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
986 done = autotable.method(do_method).call(vbox)
987 textview.grab_focus #- because if moving, focus is stolen
991 save_undo(_("move %s") % direction,
993 autotable.method(undo_method).call(vbox)
994 textview.grab_focus #- because if moving, focus is stolen
995 autoscroll_if_needed($autotable_sw, img, textview)
996 $notebook.set_page(1)
998 autotable.method(do_method).call(vbox)
999 textview.grab_focus #- because if moving, focus is stolen
1000 autoscroll_if_needed($autotable_sw, img, textview)
1001 $notebook.set_page(1)
1007 color_swap_and_cleanup = proc {
1008 perform_color_swap_and_cleanup = proc {
1009 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1010 my_gen_real_thumbnail.call
1013 cleanup_all_thumbnails.call
1014 perform_color_swap_and_cleanup.call
1016 save_undo(_("color swap"),
1018 perform_color_swap_and_cleanup.call
1020 autoscroll_if_needed($autotable_sw, img, textview)
1021 $notebook.set_page(1)
1023 perform_color_swap_and_cleanup.call
1025 autoscroll_if_needed($autotable_sw, img, textview)
1026 $notebook.set_page(1)
1031 change_frame_offset_and_cleanup_real = proc { |values|
1032 perform_change_frame_offset_and_cleanup = proc { |val|
1033 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
1034 my_gen_real_thumbnail.call
1036 perform_change_frame_offset_and_cleanup.call(values[:new])
1038 save_undo(_("specify frame offset"),
1040 perform_change_frame_offset_and_cleanup.call(values[:old])
1042 autoscroll_if_needed($autotable_sw, img, textview)
1043 $notebook.set_page(1)
1045 perform_change_frame_offset_and_cleanup.call(values[:new])
1047 autoscroll_if_needed($autotable_sw, img, textview)
1048 $notebook.set_page(1)
1053 change_frame_offset_and_cleanup = proc {
1054 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
1055 change_frame_offset_and_cleanup_real.call(values)
1059 change_pano_amount_and_cleanup_real = proc { |values|
1060 perform_change_pano_amount_and_cleanup = proc { |val|
1061 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1063 perform_change_pano_amount_and_cleanup.call(values[:new])
1065 save_undo(_("change panorama amount"),
1067 perform_change_pano_amount_and_cleanup.call(values[:old])
1069 autoscroll_if_needed($autotable_sw, img, textview)
1070 $notebook.set_page(1)
1072 perform_change_pano_amount_and_cleanup.call(values[:new])
1074 autoscroll_if_needed($autotable_sw, img, textview)
1075 $notebook.set_page(1)
1080 change_pano_amount_and_cleanup = proc {
1081 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1082 change_pano_amount_and_cleanup_real.call(values)
1086 whitebalance_and_cleanup_real = proc { |values|
1087 perform_change_whitebalance_and_cleanup = proc { |val|
1088 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1089 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1090 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1091 cleanup_all_thumbnails.call
1093 perform_change_whitebalance_and_cleanup.call(values[:new])
1095 save_undo(_("fix white balance"),
1097 perform_change_whitebalance_and_cleanup.call(values[:old])
1099 autoscroll_if_needed($autotable_sw, img, textview)
1100 $notebook.set_page(1)
1102 perform_change_whitebalance_and_cleanup.call(values[:new])
1104 autoscroll_if_needed($autotable_sw, img, textview)
1105 $notebook.set_page(1)
1110 whitebalance_and_cleanup = proc {
1111 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1112 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1113 whitebalance_and_cleanup_real.call(values)
1117 enhance_and_cleanup = proc {
1118 perform_enhance_and_cleanup = proc {
1119 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1120 my_gen_real_thumbnail.call
1123 cleanup_all_thumbnails.call
1124 perform_enhance_and_cleanup.call
1126 save_undo(_("enhance"),
1128 perform_enhance_and_cleanup.call
1130 autoscroll_if_needed($autotable_sw, img, textview)
1131 $notebook.set_page(1)
1133 perform_enhance_and_cleanup.call
1135 autoscroll_if_needed($autotable_sw, img, textview)
1136 $notebook.set_page(1)
1141 delete = proc { |isacut|
1142 if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed? This operation cannot be undone.")), { :okcancel => true })
1145 perform_delete = proc {
1146 after = autotable.get_next_widget(vbox)
1148 after = autotable.get_previous_widget(vbox)
1150 if $config['deleteondisk'] && !isacut
1151 msg 3, "scheduling for delete: #{fullpath}"
1152 $todelete << fullpath
1154 autotable.remove(vbox)
1156 $vbox2widgets[after][:textview].grab_focus
1157 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1161 previous_pos = autotable.get_current_number(vbox)
1165 delete_current_subalbum
1167 save_undo(_("delete"),
1169 autotable.reinsert(pos, vbox, filename)
1170 $notebook.set_page(1)
1171 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1173 msg 3, "removing deletion schedule of: #{fullpath}"
1174 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1177 $notebook.set_page(1)
1186 $cuts << { :vbox => vbox, :filename => filename }
1187 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1192 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1195 autotable.queue_draws << proc {
1196 $vbox2widgets[last[:vbox]][:textview].grab_focus
1197 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1199 save_undo(_("paste"),
1201 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1202 $notebook.set_page(1)
1205 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1207 $notebook.set_page(1)
1210 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1215 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1216 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1217 :whitebalance => whitebalance_and_cleanup_real, :pano => change_pano_amount_and_cleanup_real }
1219 textview.signal_connect('key-press-event') { |w, event|
1222 x, y = autotable.get_current_pos(vbox)
1223 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1224 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1225 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1226 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1228 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1229 $vbox2widgets[widget_up][:textview].grab_focus
1236 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1238 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1239 $vbox2widgets[widget_down][:textview].grab_focus
1246 if event.keyval == Gdk::Keyval::GDK_Left
1249 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1256 rotate_and_cleanup.call(-90)
1259 if event.keyval == Gdk::Keyval::GDK_Right
1260 next_ = autotable.get_next_widget(vbox)
1261 if next_ && autotable.get_current_pos(next_)[0] > x
1263 $vbox2widgets[next_][:textview].grab_focus
1270 rotate_and_cleanup.call(90)
1273 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1276 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1277 view_element(filename, { :delete => delete })
1280 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1283 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1287 !propagate #- propagate if needed
1290 $ignore_next_release = false
1291 evtbox.signal_connect('button-press-event') { |w, event|
1292 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1293 if event.state & Gdk::Window::BUTTON3_MASK != 0
1294 #- gesture redo: hold right mouse button then click left mouse button
1295 $config['nogestures'] or perform_redo
1296 $ignore_next_release = true
1298 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1300 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1302 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1303 elsif $enhance.active?
1304 enhance_and_cleanup.call
1305 elsif $delete.active?
1309 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1312 $button1_pressed_autotable = true
1313 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1314 if event.state & Gdk::Window::BUTTON1_MASK != 0
1315 #- gesture undo: hold left mouse button then click right mouse button
1316 $config['nogestures'] or perform_undo
1317 $ignore_next_release = true
1319 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1320 view_element(filename, { :delete => delete })
1325 evtbox.signal_connect('button-release-event') { |w, event|
1326 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1327 if !$ignore_next_release
1328 x, y = autotable.get_current_pos(vbox)
1329 next_ = autotable.get_next_widget(vbox)
1330 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1331 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1332 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1333 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1334 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1335 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1336 :pano => change_pano_amount_and_cleanup })
1338 $ignore_next_release = false
1339 $gesture_press = nil
1344 #- handle reordering with drag and drop
1345 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1346 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1347 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1348 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1351 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1353 #- mouse gesture first (dnd disables button-release-event)
1354 if $gesture_press && $gesture_press[:filename] == filename
1355 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1356 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1357 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1358 rotate_and_cleanup.call(angle)
1359 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1361 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1362 msg 3, "gesture delete: click-drag right button to the bottom"
1364 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1369 ctxt.targets.each { |target|
1370 if target.name == 'reorder-elements'
1371 move_dnd = proc { |from,to|
1374 autotable.move(from, to)
1375 save_undo(_("reorder"),
1378 autotable.move(to - 1, from)
1380 autotable.move(to, from + 1)
1382 $notebook.set_page(1)
1384 autotable.move(from, to)
1385 $notebook.set_page(1)
1390 if $multiple_dnd.size == 0
1391 move_dnd.call(selection_data.data.to_i,
1392 autotable.get_current_number(vbox))
1394 UndoHandler.begin_batch
1395 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1397 #- need to update current position between each call
1398 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1399 autotable.get_current_number(vbox))
1401 UndoHandler.end_batch
1412 def create_auto_table
1414 $autotable = Gtk::AutoTable.new(5)
1416 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1417 thumbnails_vb = Gtk::VBox.new(false, 5)
1419 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1420 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1421 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1422 thumbnails_vb.add($autotable)
1424 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1425 $autotable_sw.add_with_viewport(thumbnails_vb)
1427 #- follows stuff for handling multiple elements selection
1428 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1430 update_selected = proc {
1431 $autotable.current_order.each { |path|
1432 w = $name2widgets[path][:evtbox].window
1433 xm = w.position[0] + w.size[0]/2
1434 ym = w.position[1] + w.size[1]/2
1435 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1436 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1437 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1438 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1441 if $selected_elements[path] && ! $selected_elements[path][:keep]
1442 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))
1443 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1444 $selected_elements.delete(path)
1449 $autotable.signal_connect('realize') { |w,e|
1450 gc = Gdk::GC.new($autotable.window)
1451 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1452 gc.function = Gdk::GC::INVERT
1453 #- autoscroll handling for DND and multiple selections
1454 Gtk.timeout_add(100) {
1455 if ! $autotable.window.nil?
1456 w, x, y, mask = $autotable.window.pointer
1457 if mask & Gdk::Window::BUTTON1_MASK != 0
1458 if y < $autotable_sw.vadjustment.value
1460 $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]])
1462 if $button1_pressed_autotable || press_x
1463 scroll_upper($autotable_sw, y)
1466 w, pos_x, pos_y = $autotable.window.pointer
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 update_selected.call
1471 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1473 $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]])
1475 if $button1_pressed_autotable || press_x
1476 scroll_lower($autotable_sw, y)
1479 w, pos_x, pos_y = $autotable.window.pointer
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]])
1481 update_selected.call
1486 ! $autotable.window.nil?
1490 $autotable.signal_connect('button-press-event') { |w,e|
1492 if !$button1_pressed_autotable
1495 if e.state & Gdk::Window::SHIFT_MASK == 0
1496 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1497 $selected_elements = {}
1498 $statusbar.push(0, utf8(_("Nothing selected.")))
1500 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1502 set_mousecursor(Gdk::Cursor::TCROSS)
1506 $autotable.signal_connect('button-release-event') { |w,e|
1508 if $button1_pressed_autotable
1509 #- unselect all only now
1510 $multiple_dnd = $selected_elements.keys
1511 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1512 $selected_elements = {}
1513 $button1_pressed_autotable = false
1516 $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]])
1517 if $selected_elements.length > 0
1518 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1521 press_x = press_y = pos_x = pos_y = nil
1522 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1526 $autotable.signal_connect('motion-notify-event') { |w,e|
1529 $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]])
1533 $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]])
1534 update_selected.call
1540 def create_subalbums_page
1542 subalbums_hb = Gtk::HBox.new
1543 $subalbums_vb = Gtk::VBox.new(false, 5)
1544 subalbums_hb.pack_start($subalbums_vb, false, false)
1545 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1546 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1547 $subalbums_sw.add_with_viewport(subalbums_hb)
1550 def save_current_file
1556 ios = File.open($filename, "w")
1557 $xmldoc.write(ios, 0)
1559 rescue Iconv::IllegalSequence
1560 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1561 if ! ios.nil? && ! ios.closed?
1564 $xmldoc.xml_decl.encoding = 'UTF-8'
1565 ios = File.open($filename, "w")
1566 $xmldoc.write(ios, 0)
1576 def save_current_file_user
1577 save_tempfilename = $filename
1578 $filename = $orig_filename
1579 if ! save_current_file
1580 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1581 $filename = save_tempfilename
1585 $generated_outofline = false
1586 $filename = save_tempfilename
1588 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1589 $todelete.each { |f|
1590 system("rm -f #{f}")
1594 def mark_document_as_dirty
1595 $xmldoc.elements.each('//dir') { |elem|
1596 elem.delete_attribute('already-generated')
1600 #- ret: true => ok false => cancel
1601 def ask_save_modifications(msg1, msg2, *options)
1603 options = options.size > 0 ? options[0] : {}
1605 if options[:disallow_cancel]
1606 dialog = Gtk::Dialog.new(msg1,
1608 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1609 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1610 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1612 dialog = Gtk::Dialog.new(msg1,
1614 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1615 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1616 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1617 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1619 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1620 dialog.vbox.add(Gtk::Label.new(msg2))
1621 dialog.window_position = Gtk::Window::POS_CENTER
1624 dialog.run { |response|
1626 if response == Gtk::Dialog::RESPONSE_YES
1627 if ! save_current_file_user
1628 return ask_save_modifications(msg1, msg2, options)
1631 #- if we have generated an album but won't save modifications, we must remove
1632 #- already-generated markers in original file
1633 if $generated_outofline
1635 $xmldoc = REXML::Document.new File.new($orig_filename)
1636 mark_document_as_dirty
1637 ios = File.open($orig_filename, "w")
1638 $xmldoc.write(ios, 0)
1641 puts "exception: #{$!}"
1645 if response == Gtk::Dialog::RESPONSE_CANCEL
1648 $todelete = [] #- unconditionally clear the list of images/videos to delete
1654 def try_quit(*options)
1655 if ask_save_modifications(utf8(_("Save before quitting?")),
1656 utf8(_("Do you want to save your changes before quitting?")),
1662 def show_popup(parent, msg, *options)
1663 dialog = Gtk::Dialog.new
1664 if options[0] && options[0][:title]
1665 dialog.title = options[0][:title]
1667 dialog.title = utf8(_("Booh message"))
1669 lbl = Gtk::Label.new
1670 if options[0] && options[0][:nomarkup]
1675 if options[0] && options[0][:centered]
1676 lbl.set_justify(Gtk::Justification::CENTER)
1678 if options[0] && options[0][:selectable]
1679 lbl.selectable = true
1681 if options[0] && options[0][:topwidget]
1682 dialog.vbox.add(options[0][:topwidget])
1684 if options[0] && options[0][:scrolled]
1685 sw = Gtk::ScrolledWindow.new(nil, nil)
1686 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1687 sw.add_with_viewport(lbl)
1689 dialog.set_default_size(400, 500)
1691 dialog.vbox.add(lbl)
1692 dialog.set_default_size(200, 120)
1694 if options[0] && options[0][:okcancel]
1695 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1697 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1699 if options[0] && options[0][:pos_centered]
1700 dialog.window_position = Gtk::Window::POS_CENTER
1702 dialog.window_position = Gtk::Window::POS_MOUSE
1705 if options[0] && options[0][:linkurl]
1706 linkbut = Gtk::Button.new('')
1707 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1708 linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1709 linkbut.relief = Gtk::RELIEF_NONE
1710 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1711 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1712 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1717 if !options[0] || !options[0][:not_transient]
1718 dialog.transient_for = parent
1719 dialog.run { |response|
1721 if options[0] && options[0][:okcancel]
1722 return response == Gtk::Dialog::RESPONSE_OK
1726 dialog.signal_connect('response') { dialog.destroy }
1730 def backend_wait_message(parent, msg, infopipe_path, mode)
1732 w.set_transient_for(parent)
1735 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1736 vb.pack_start(Gtk::Label.new(msg), false, false)
1738 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1739 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1740 if mode != 'one dir scan'
1741 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1743 if mode == 'web-album'
1744 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1745 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1747 vb.pack_start(Gtk::HSeparator.new, false, false)
1749 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1750 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1751 vb.pack_end(bottom, false, false)
1753 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1754 refresh_thread = Thread.new {
1755 directories_counter = 0
1756 while line = infopipe.gets
1757 if line =~ /^directories: (\d+), sizes: (\d+)/
1758 directories = $1.to_f + 1
1760 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1761 elements = $3.to_f + 1
1762 if mode == 'web-album'
1766 gtk_thread_protect { pb1_1.fraction = 0 }
1767 if mode != 'one dir scan'
1768 newtext = utf8(full_src_dir_to_rel($1, $2))
1769 newtext = '/' if newtext == ''
1770 gtk_thread_protect { pb1_2.text = newtext }
1771 directories_counter += 1
1772 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1774 elsif line =~ /^processing element$/
1775 element_counter += 1
1776 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1777 elsif line =~ /^processing size$/
1778 element_counter += 1
1779 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1780 elsif line =~ /^finished processing sizes$/
1781 gtk_thread_protect { pb1_1.fraction = 1 }
1782 elsif line =~ /^creating index.html$/
1783 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1784 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1785 directories_counter = 0
1786 elsif line =~ /^index.html: (.+)\|(.+)/
1787 newtext = utf8(full_src_dir_to_rel($1, $2))
1788 newtext = '/' if newtext == ''
1789 gtk_thread_protect { pb2.text = newtext }
1790 directories_counter += 1
1791 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1792 elsif line =~ /^die: (.*)$/
1799 w.signal_connect('delete-event') { w.destroy }
1800 w.signal_connect('destroy') {
1801 Thread.kill(refresh_thread)
1802 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1805 system("rm -f #{infopipe_path}")
1808 w.window_position = Gtk::Window::POS_CENTER
1814 def call_backend(cmd, waitmsg, mode, params)
1815 pipe = Tempfile.new("boohpipe")
1817 system("mkfifo #{pipe.path}")
1818 cmd += " --info-pipe #{pipe.path}"
1819 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
1824 id, exitstatus = Process.waitpid2(pid)
1825 gtk_thread_protect { w8.destroy }
1827 if params[:successmsg]
1828 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
1830 if params[:closure_after]
1831 gtk_thread_protect(¶ms[:closure_after])
1833 elsif exitstatus == 15
1834 #- say nothing, user aborted
1836 gtk_thread_protect { show_popup($main_window,
1837 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
1843 button.signal_connect('clicked') {
1844 Process.kill('SIGTERM', pid)
1848 def save_changes(*forced)
1849 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1853 $xmldir.delete_attribute('already-generated')
1855 propagate_children = proc { |xmldir|
1856 if xmldir.attributes['subdirs-caption']
1857 xmldir.delete_attribute('already-generated')
1859 xmldir.elements.each('dir') { |element|
1860 propagate_children.call(element)
1864 if $xmldir.child_byname_notattr('dir', 'deleted')
1865 new_title = $subalbums_title.buffer.text
1866 if new_title != $xmldir.attributes['subdirs-caption']
1867 parent = $xmldir.parent
1868 if parent.name == 'dir'
1869 parent.delete_attribute('already-generated')
1871 propagate_children.call($xmldir)
1873 $xmldir.add_attribute('subdirs-caption', new_title)
1874 $xmldir.elements.each('dir') { |element|
1875 if !element.attributes['deleted']
1876 path = element.attributes['path']
1877 newtext = $subalbums_edits[path][:editzone].buffer.text
1878 if element.attributes['subdirs-caption']
1879 if element.attributes['subdirs-caption'] != newtext
1880 propagate_children.call(element)
1882 element.add_attribute('subdirs-caption', newtext)
1883 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1885 if element.attributes['thumbnails-caption'] != newtext
1886 element.delete_attribute('already-generated')
1888 element.add_attribute('thumbnails-caption', newtext)
1889 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1895 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
1896 if $xmldir.attributes['thumbnails-caption']
1897 path = $xmldir.attributes['path']
1898 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1900 elsif $xmldir.attributes['thumbnails-caption']
1901 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
1904 #- remove and reinsert elements to reflect new ordering
1907 $xmldir.elements.each { |element|
1908 if element.name == 'image' || element.name == 'video'
1909 saves[element.attributes['filename']] = element.remove
1913 $autotable.current_order.each { |path|
1914 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1915 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1918 saves.each_key { |path|
1919 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1920 chld.add_attribute('deleted', 'true')
1924 def sort_by_exif_date
1928 $xmldir.elements.each { |element|
1929 if element.name == 'image' || element.name == 'video'
1930 current_order << element.attributes['filename']
1934 #- look for EXIF dates
1936 w.set_transient_for($main_window)
1938 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1939 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
1940 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
1941 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1942 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1943 vb.pack_end(bottom, false, false)
1945 w.signal_connect('delete-event') { w.destroy }
1946 w.window_position = Gtk::Window::POS_CENTER
1950 b.signal_connect('clicked') { aborted = true }
1953 current_order.each { |f|
1955 if entry2type(f) == 'image'
1957 pb.fraction = i.to_f / current_order.size
1958 Gtk.main_iteration while Gtk.events_pending?
1959 date_time = `identify -format "%[EXIF:DateTime]" '#{from_utf8($current_path + "/" + f)}'`.chomp
1960 if $? == 0 && date_time != ''
1961 dates[f] = date_time
1974 $xmldir.elements.each { |element|
1975 if element.name == 'image' || element.name == 'video'
1976 saves[element.attributes['filename']] = element.remove
1980 #- find a good fallback for all entries without a date (still next to the item they were next to)
1981 neworder = dates.keys.sort { |a,b| dates[a] <=> dates[b] }
1982 for i in 0 .. current_order.size - 1
1983 if ! neworder.include?(current_order[i])
1985 while j > 0 && ! neworder.include?(current_order[j])
1988 neworder[(neworder.index(current_order[j]) || -1 ) + 1, 0] = current_order[i]
1992 $xmldir.add_element(saves[f].name, saves[f].attributes)
1995 #- let the auto-table reflect new ordering
1999 def remove_all_captions
2002 $autotable.current_order.each { |path|
2003 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2004 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2006 save_undo(_("remove all captions"),
2008 texts.each_key { |key|
2009 $name2widgets[key][:textview].buffer.text = texts[key]
2011 $notebook.set_page(1)
2013 texts.each_key { |key|
2014 $name2widgets[key][:textview].buffer.text = ''
2016 $notebook.set_page(1)
2022 $selected_elements.each_key { |path|
2023 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2029 $selected_elements = {}
2033 $undo_tb.sensitive = $undo_mb.sensitive = false
2034 $redo_tb.sensitive = $redo_mb.sensitive = false
2040 $subalbums_vb.children.each { |chld|
2041 $subalbums_vb.remove(chld)
2043 $subalbums = Gtk::Table.new(0, 0, true)
2044 current_y_sub_albums = 0
2046 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2047 $subalbums_edits = {}
2048 subalbums_counter = 0
2049 subalbums_edits_bypos = {}
2051 add_subalbum = proc { |xmldir, counter|
2052 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2053 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2054 if xmldir == $xmldir
2055 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2056 caption = xmldir.attributes['thumbnails-caption']
2057 captionfile, dummy = find_subalbum_caption_info(xmldir)
2058 infotype = 'thumbnails'
2060 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2061 captionfile, caption = find_subalbum_caption_info(xmldir)
2062 infotype = find_subalbum_info_type(xmldir)
2064 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2065 hbox = Gtk::HBox.new
2066 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2068 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2071 my_gen_real_thumbnail = proc {
2072 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2075 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2076 f.add(img = Gtk::Image.new)
2077 my_gen_real_thumbnail.call
2079 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2081 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2082 $subalbums.attach(hbox,
2083 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2085 frame, textview = create_editzone($subalbums_sw, 0, img)
2086 textview.buffer.text = caption
2087 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2088 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2090 change_image = proc {
2091 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2093 Gtk::FileChooser::ACTION_OPEN,
2095 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2096 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2097 fc.transient_for = $main_window
2098 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))
2099 f.add(preview_img = Gtk::Image.new)
2101 fc.signal_connect('update-preview') { |w|
2103 if fc.preview_filename
2104 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2105 fc.preview_widget_active = true
2107 rescue Gdk::PixbufError
2108 fc.preview_widget_active = false
2111 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2113 old_file = captionfile
2114 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2115 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2116 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2117 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2119 new_file = fc.filename
2120 msg 3, "new captionfile is: #{fc.filename}"
2121 perform_changefile = proc {
2122 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2123 $modified_pixbufs.delete(thumbnail_file)
2124 xmldir.delete_attribute("#{infotype}-rotate")
2125 xmldir.delete_attribute("#{infotype}-color-swap")
2126 xmldir.delete_attribute("#{infotype}-enhance")
2127 xmldir.delete_attribute("#{infotype}-frame-offset")
2128 my_gen_real_thumbnail.call
2130 perform_changefile.call
2132 save_undo(_("change caption file for sub-album"),
2134 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2135 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2136 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2137 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2138 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2139 my_gen_real_thumbnail.call
2140 $notebook.set_page(0)
2142 perform_changefile.call
2143 $notebook.set_page(0)
2150 rotate_and_cleanup = proc { |angle|
2151 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2152 system("rm -f '#{thumbnail_file}'")
2155 move = proc { |direction|
2158 save_changes('forced')
2159 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2160 if direction == 'up'
2161 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2162 subalbums_edits_bypos[oldpos - 1][:position] += 1
2164 if direction == 'down'
2165 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2166 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2168 if direction == 'top'
2169 for i in 1 .. oldpos - 1
2170 subalbums_edits_bypos[i][:position] += 1
2172 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2174 if direction == 'bottom'
2175 for i in oldpos + 1 .. subalbums_counter
2176 subalbums_edits_bypos[i][:position] -= 1
2178 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2182 $xmldir.elements.each('dir') { |element|
2183 if (!element.attributes['deleted'])
2184 elems << [ element.attributes['path'], element.remove ]
2187 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2188 each { |e| $xmldir.add_element(e[1]) }
2189 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2190 $xmldir.elements.each('descendant::dir') { |elem|
2191 elem.delete_attribute('already-generated')
2194 sel = $albums_tv.selection.selected_rows
2196 populate_subalbums_treeview(false)
2197 $albums_tv.selection.select_path(sel[0])
2200 color_swap_and_cleanup = proc {
2201 perform_color_swap_and_cleanup = proc {
2202 color_swap(xmldir, "#{infotype}-")
2203 my_gen_real_thumbnail.call
2205 perform_color_swap_and_cleanup.call
2207 save_undo(_("color swap"),
2209 perform_color_swap_and_cleanup.call
2210 $notebook.set_page(0)
2212 perform_color_swap_and_cleanup.call
2213 $notebook.set_page(0)
2218 change_frame_offset_and_cleanup = proc {
2219 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2220 perform_change_frame_offset_and_cleanup = proc { |val|
2221 change_frame_offset(xmldir, "#{infotype}-", val)
2222 my_gen_real_thumbnail.call
2224 perform_change_frame_offset_and_cleanup.call(values[:new])
2226 save_undo(_("specify frame offset"),
2228 perform_change_frame_offset_and_cleanup.call(values[:old])
2229 $notebook.set_page(0)
2231 perform_change_frame_offset_and_cleanup.call(values[:new])
2232 $notebook.set_page(0)
2238 whitebalance_and_cleanup = proc {
2239 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2240 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2241 perform_change_whitebalance_and_cleanup = proc { |val|
2242 change_whitebalance(xmldir, "#{infotype}-", val)
2243 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2244 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2245 system("rm -f '#{thumbnail_file}'")
2247 perform_change_whitebalance_and_cleanup.call(values[:new])
2249 save_undo(_("fix white balance"),
2251 perform_change_whitebalance_and_cleanup.call(values[:old])
2252 $notebook.set_page(0)
2254 perform_change_whitebalance_and_cleanup.call(values[:new])
2255 $notebook.set_page(0)
2261 enhance_and_cleanup = proc {
2262 perform_enhance_and_cleanup = proc {
2263 enhance(xmldir, "#{infotype}-")
2264 my_gen_real_thumbnail.call
2267 perform_enhance_and_cleanup.call
2269 save_undo(_("enhance"),
2271 perform_enhance_and_cleanup.call
2272 $notebook.set_page(0)
2274 perform_enhance_and_cleanup.call
2275 $notebook.set_page(0)
2280 evtbox.signal_connect('button-press-event') { |w, event|
2281 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2283 rotate_and_cleanup.call(90)
2285 rotate_and_cleanup.call(-90)
2286 elsif $enhance.active?
2287 enhance_and_cleanup.call
2290 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2291 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2292 { :forbid_left => true, :forbid_right => true,
2293 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2294 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2295 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2296 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
2298 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2303 evtbox.signal_connect('button-press-event') { |w, event|
2304 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2308 evtbox.signal_connect('button-release-event') { |w, event|
2309 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2310 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2311 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2312 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2313 msg 3, "gesture rotate: #{angle}"
2314 rotate_and_cleanup.call(angle)
2317 $gesture_press = nil
2320 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2321 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2322 current_y_sub_albums += 1
2325 if $xmldir.child_byname_notattr('dir', 'deleted')
2327 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2328 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2329 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2330 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2331 #- this album image/caption
2332 if $xmldir.attributes['thumbnails-caption']
2333 add_subalbum.call($xmldir, 0)
2336 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2337 $xmldir.elements.each { |element|
2338 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2339 #- element (image or video) of this album
2340 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2341 msg 3, "dest_img: #{dest_img}"
2342 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2343 total[element.name] += 1
2345 if element.name == 'dir' && !element.attributes['deleted']
2346 #- sub-album image/caption
2347 add_subalbum.call(element, subalbums_counter += 1)
2348 total[element.name] += 1
2351 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2352 total['image'], total['video'], total['dir'] ]))
2353 $subalbums_vb.add($subalbums)
2354 $subalbums_vb.show_all
2356 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2357 $notebook.get_tab_label($autotable_sw).sensitive = false
2358 $notebook.set_page(0)
2359 $thumbnails_title.buffer.text = ''
2361 $notebook.get_tab_label($autotable_sw).sensitive = true
2362 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2365 if !$xmldir.child_byname_notattr('dir', 'deleted')
2366 $notebook.get_tab_label($subalbums_sw).sensitive = false
2367 $notebook.set_page(1)
2369 $notebook.get_tab_label($subalbums_sw).sensitive = true
2373 def pixbuf_or_nil(filename)
2375 return Gdk::Pixbuf.new(filename)
2381 def theme_choose(current)
2382 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2384 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2385 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2386 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2388 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2389 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2390 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2391 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2392 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2393 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2394 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2395 treeview.signal_connect('button-press-event') { |w, event|
2396 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2397 dialog.response(Gtk::Dialog::RESPONSE_OK)
2401 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2403 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2406 iter[0] = File.basename(dir)
2407 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2408 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2409 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2410 if File.basename(dir) == current
2411 treeview.selection.select_iter(iter)
2415 dialog.set_default_size(700, 400)
2416 dialog.vbox.show_all
2417 dialog.run { |response|
2418 iter = treeview.selection.selected
2420 if response == Gtk::Dialog::RESPONSE_OK && iter
2421 return model.get_value(iter, 0)
2427 def show_password_protections
2428 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2429 child_iter = $albums_iters[xmldir.attributes['path']]
2430 if xmldir.attributes['password-protect']
2431 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2432 already_protected = true
2433 elsif already_protected
2434 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2436 pix = pix.saturate_and_pixelate(1, true)
2442 xmldir.elements.each('dir') { |elem|
2443 if !elem.attributes['deleted']
2444 examine_dir_elem.call(child_iter, elem, already_protected)
2448 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2451 def populate_subalbums_treeview(select_first)
2455 $subalbums_vb.children.each { |chld|
2456 $subalbums_vb.remove(chld)
2459 source = $xmldoc.root.attributes['source']
2460 msg 3, "source: #{source}"
2462 xmldir = $xmldoc.elements['//dir']
2463 if !xmldir || xmldir.attributes['path'] != source
2464 msg 1, _("Corrupted booh file...")
2468 append_dir_elem = proc { |parent_iter, xmldir|
2469 child_iter = $albums_ts.append(parent_iter)
2470 child_iter[0] = File.basename(xmldir.attributes['path'])
2471 child_iter[1] = xmldir.attributes['path']
2472 $albums_iters[xmldir.attributes['path']] = child_iter
2473 msg 3, "puttin location: #{xmldir.attributes['path']}"
2474 xmldir.elements.each('dir') { |elem|
2475 if !elem.attributes['deleted']
2476 append_dir_elem.call(child_iter, elem)
2480 append_dir_elem.call(nil, xmldir)
2481 show_password_protections
2483 $albums_tv.expand_all
2485 $albums_tv.selection.select_iter($albums_ts.iter_first)
2489 def open_file(filename)
2493 $current_path = nil #- invalidate
2494 $modified_pixbufs = {}
2497 $subalbums_vb.children.each { |chld|
2498 $subalbums_vb.remove(chld)
2501 if !File.exists?(filename)
2502 return utf8(_("File not found."))
2506 $xmldoc = REXML::Document.new File.new(filename)
2511 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2512 if entry2type(filename).nil?
2513 return utf8(_("Not a booh file!"))
2515 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."))
2519 if !source = $xmldoc.root.attributes['source']
2520 return utf8(_("Corrupted booh file..."))
2523 if !dest = $xmldoc.root.attributes['destination']
2524 return utf8(_("Corrupted booh file..."))
2527 if !theme = $xmldoc.root.attributes['theme']
2528 return utf8(_("Corrupted booh file..."))
2531 if $xmldoc.root.attributes['version'] < '0.8.4'
2532 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2533 mark_document_as_dirty
2534 if $xmldoc.root.attributes['version'] < '0.8.4'
2535 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2536 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2537 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2538 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2539 if old_dest_dir != new_dest_dir
2540 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2542 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2543 xmldir.elements.each { |element|
2544 if %w(image video).include?(element.name) && !element.attributes['deleted']
2545 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2546 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2547 Dir[old_name + '*'].each { |file|
2548 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2549 file != new_file and sys("mv '#{file}' '#{new_file}'")
2552 if element.name == 'dir' && !element.attributes['deleted']
2553 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2554 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2555 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2559 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2563 $xmldoc.root.add_attribute('version', $VERSION)
2566 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2567 optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2568 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2570 $filename = filename
2571 select_theme(theme, limit_sizes, optimizefor32, nperrow)
2572 $default_size['thumbnails'] =~ /(.*)x(.*)/
2573 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2574 $albums_thumbnail_size =~ /(.*)x(.*)/
2575 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2577 populate_subalbums_treeview(true)
2579 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
2583 def open_file_user(filename)
2584 result = open_file(filename)
2586 $config['last-opens'] ||= []
2587 if $config['last-opens'][-1] != utf8(filename)
2588 $config['last-opens'] << utf8(filename)
2590 $orig_filename = $filename
2591 tmp = Tempfile.new("boohtemp")
2594 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2596 $tempfiles << $filename << "#{$filename}.backup"
2598 $orig_filename = nil
2604 if !ask_save_modifications(utf8(_("Save this album?")),
2605 utf8(_("Do you want to save the changes to this album?")),
2606 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2609 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2611 Gtk::FileChooser::ACTION_OPEN,
2613 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2614 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2615 fc.set_current_folder(File.expand_path("~/.booh"))
2616 fc.transient_for = $main_window
2619 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2620 push_mousecursor_wait(fc)
2621 msg = open_file_user(fc.filename)
2637 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2642 def additional_booh_options
2645 options += "--mproc #{$config['mproc'].to_i} "
2647 if $config['emptycomments']
2648 options += "--empty-comments "
2654 if !ask_save_modifications(utf8(_("Save this album?")),
2655 utf8(_("Do you want to save the changes to this album?")),
2656 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2659 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2661 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2662 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2663 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2665 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2666 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2667 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2668 tbl.attach(src = Gtk::Entry.new,
2669 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2670 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2671 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2672 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2673 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2674 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2675 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2676 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2677 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2678 tbl.attach(dest = Gtk::Entry.new,
2679 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2680 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2681 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2682 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2683 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2684 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2685 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2686 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2687 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2689 tooltips = Gtk::Tooltips.new
2690 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2691 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2692 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2693 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2694 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2695 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2696 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)
2697 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2698 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2699 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2700 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2701 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)
2703 src_nb_calculated_for = ''
2705 process_src_nb = proc {
2706 if src.text != src_nb_calculated_for
2707 src_nb_calculated_for = src.text
2709 Thread.kill(src_nb_thread)
2712 if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2713 if File.readable?(from_utf8(src_nb_calculated_for))
2714 src_nb_thread = Thread.new {
2715 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2716 total = { 'image' => 0, 'video' => 0, nil => 0 }
2717 `find '#{from_utf8(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2718 if File.basename(dir) =~ /^\./
2722 Dir.entries(dir.chomp).each { |file|
2723 total[entry2type(file)] += 1
2725 rescue Errno::EACCES, Errno::ENOENT
2729 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2733 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2736 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2741 timeout_src_nb = Gtk.timeout_add(100) {
2745 src_browse.signal_connect('clicked') {
2746 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2748 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2750 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2751 fc.transient_for = $main_window
2752 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2753 src.text = utf8(fc.filename)
2755 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2760 dest_browse.signal_connect('clicked') {
2761 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2763 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2765 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2766 fc.transient_for = $main_window
2767 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2768 dest.text = utf8(fc.filename)
2773 conf_browse.signal_connect('clicked') {
2774 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2776 Gtk::FileChooser::ACTION_SAVE,
2778 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2779 fc.transient_for = $main_window
2780 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2781 fc.set_current_folder(File.expand_path("~/.booh"))
2782 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2783 conf.text = utf8(fc.filename)
2790 recreate_theme_config = proc {
2791 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2793 select_theme(theme_button.label, 'all', optimize432.active?, nil)
2794 $images_size.each { |s|
2795 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2799 tooltips.set_tip(cb, utf8(s['description']), nil)
2800 theme_sizes << { :widget => cb, :value => s['name'] }
2802 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2803 tooltips = Gtk::Tooltips.new
2804 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2805 theme_sizes << { :widget => cb, :value => 'original' }
2808 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2811 $allowed_N_values.each { |n|
2813 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2815 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2817 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2821 nperrows << { :widget => rb, :value => n }
2823 nperrowradios.show_all
2825 recreate_theme_config.call
2827 theme_button.signal_connect('clicked') {
2828 if newtheme = theme_choose(theme_button.label)
2829 theme_button.label = newtheme
2830 recreate_theme_config.call
2834 dialog.vbox.add(frame1)
2835 dialog.vbox.add(frame2)
2836 dialog.window_position = Gtk::Window::POS_MOUSE
2842 dialog.run { |response|
2843 if response == Gtk::Dialog::RESPONSE_OK
2844 srcdir = from_utf8(src.text)
2845 destdir = from_utf8(dest.text)
2846 if !File.directory?(srcdir)
2847 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2849 elsif conf.text == ''
2850 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2852 elsif File.directory?(from_utf8(conf.text))
2853 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
2855 elsif destdir != make_dest_filename(destdir)
2856 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2858 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
2859 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2861 elsif File.exists?(destdir) && !File.directory?(destdir)
2862 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2864 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2865 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2867 system("mkdir '#{destdir}'")
2868 if !File.directory?(destdir)
2869 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2880 srcdir = from_utf8(src.text)
2881 destdir = from_utf8(dest.text)
2882 configskel = File.expand_path(from_utf8(conf.text))
2883 theme = theme_button.label
2884 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2885 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2886 opt432 = optimize432.active?
2887 madewith = madewithentry.text
2889 Thread.kill(src_nb_thread)
2890 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2893 Gtk.timeout_remove(timeout_src_nb)
2896 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2897 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2898 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' #{additional_booh_options}",
2899 utf8(_("Please wait while scanning source directory...")),
2901 { :closure_after => proc { open_file_user(configskel) } })
2906 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2908 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2909 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2910 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2912 source = $xmldoc.root.attributes['source']
2913 dest = $xmldoc.root.attributes['destination']
2914 theme = $xmldoc.root.attributes['theme']
2915 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2916 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2917 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2919 limit_sizes = limit_sizes.split(/,/)
2921 madewith = $xmldoc.root.attributes['made-with']
2923 tooltips = Gtk::Tooltips.new
2924 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2925 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2926 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2927 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2928 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2929 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2930 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2931 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
2932 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2933 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2934 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2935 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
2936 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2938 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2939 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2940 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2941 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2942 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2943 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2944 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)
2945 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2946 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2947 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2948 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2950 madewithentry.text = madewith
2952 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)
2956 recreate_theme_config = proc {
2957 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2959 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
2961 $images_size.each { |s|
2962 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2964 if limit_sizes.include?(s['name'])
2972 tooltips.set_tip(cb, utf8(s['description']), nil)
2973 theme_sizes << { :widget => cb, :value => s['name'] }
2975 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2976 tooltips = Gtk::Tooltips.new
2977 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2978 if limit_sizes && limit_sizes.include?('original')
2981 theme_sizes << { :widget => cb, :value => 'original' }
2984 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2987 $allowed_N_values.each { |n|
2989 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2991 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2993 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2994 nperrowradios.add(Gtk::Label.new(' '))
2995 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
2998 nperrows << { :widget => rb, :value => n.to_s }
3000 nperrowradios.show_all
3002 recreate_theme_config.call
3004 theme_button.signal_connect('clicked') {
3005 if newtheme = theme_choose(theme_button.label)
3008 theme_button.label = newtheme
3009 recreate_theme_config.call
3013 dialog.vbox.add(frame1)
3014 dialog.vbox.add(frame2)
3015 dialog.window_position = Gtk::Window::POS_MOUSE
3021 dialog.run { |response|
3022 if response == Gtk::Dialog::RESPONSE_OK
3023 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3024 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3033 save_theme = theme_button.label
3034 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3035 save_opt432 = optimize432.active?
3036 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3037 save_madewith = madewithentry.text
3040 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith)
3041 mark_document_as_dirty
3043 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3044 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3045 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' #{additional_booh_options}",
3046 utf8(_("Please wait while scanning source directory...")),
3048 { :closure_after => proc {
3049 open_file($filename)
3058 sel = $albums_tv.selection.selected_rows
3060 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3061 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3062 utf8(_("Please wait while scanning source directory...")),
3064 { :closure_after => proc {
3065 open_file($filename)
3066 $albums_tv.selection.select_path(sel[0])
3074 sel = $albums_tv.selection.selected_rows
3076 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3077 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3078 utf8(_("Please wait while scanning source directory...")),
3080 { :closure_after => proc {
3081 open_file($filename)
3082 $albums_tv.selection.select_path(sel[0])
3090 theme = $xmldoc.root.attributes['theme']
3091 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3093 limit_sizes = "--sizes #{limit_sizes}"
3095 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3096 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3097 utf8(_("Please wait while scanning source directory...")),
3099 { :closure_after => proc {
3100 open_file($filename)
3106 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3108 Gtk::FileChooser::ACTION_SAVE,
3110 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3111 fc.transient_for = $main_window
3112 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3113 fc.set_current_folder(File.expand_path("~/.booh"))
3114 fc.filename = $orig_filename
3115 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3116 $orig_filename = fc.filename
3117 if ! save_current_file_user
3121 $config['last-opens'] ||= []
3122 $config['last-opens'] << $orig_filename
3128 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3130 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3131 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3132 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3134 dialog.vbox.add(notebook = Gtk::Notebook.new)
3135 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3136 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3137 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3138 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
3139 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3140 tooltips = Gtk::Tooltips.new
3141 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3142 for example: /usr/bin/mplayer %f")), nil)
3143 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3144 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3145 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3146 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3147 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3148 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3149 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3150 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3151 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)),
3152 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3153 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)
3154 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3155 0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3156 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)
3157 tbl.attach(emptycomments_check = Gtk::CheckButton.new(utf8(_("Use empty comments for new albums"))),
3158 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3159 tooltips.set_tip(emptycomments_check, utf8(_("Normally, filenames are used as comments for new albums. Check this if you prefer empty comments.")), nil)
3160 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3161 0, 2, 5, 6, Gtk::FILL, Gtk::SHRINK, 2, 2)
3162 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)
3163 smp_check.signal_connect('toggled') {
3164 if smp_check.active?
3165 smp_hbox.sensitive = true
3167 smp_hbox.sensitive = false
3171 smp_check.active = true
3172 smp_spin.value = $config['mproc'].to_i
3174 nogestures_check.active = $config['nogestures']
3175 emptycomments_check.active = $config['emptycomments']
3176 deleteondisk_check.active = $config['deleteondisk']
3178 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3179 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3180 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3181 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3182 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3184 dialog.vbox.show_all
3185 dialog.run { |response|
3186 if response == Gtk::Dialog::RESPONSE_OK
3187 $config['video-viewer'] = video_viewer_entry.text
3188 $config['browser'] = browser_entry.text
3189 if smp_check.active?
3190 $config['mproc'] = smp_spin.value.to_i
3192 $config.delete('mproc')
3194 $config['nogestures'] = nogestures_check.active?
3195 $config['emptycomments'] = emptycomments_check.active?
3196 $config['deleteondisk'] = deleteondisk_check.active?
3198 $config['convert-enhance'] = enhance_entry.text
3205 if $undo_tb.sensitive?
3206 $redo_tb.sensitive = $redo_mb.sensitive = true
3207 if not more_undoes = UndoHandler.undo($statusbar)
3208 $undo_tb.sensitive = $undo_mb.sensitive = false
3214 if $redo_tb.sensitive?
3215 $undo_tb.sensitive = $undo_mb.sensitive = true
3216 if not more_redoes = UndoHandler.redo($statusbar)
3217 $redo_tb.sensitive = $redo_mb.sensitive = false
3222 def show_one_click_explanation(intro)
3223 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
3225 %s When such a tool is activated
3226 (<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
3227 on a thumbnail will immediately apply the desired action.
3229 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
3235 GNU GENERAL PUBLIC LICENSE
3236 Version 2, June 1991
3238 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
3239 675 Mass Ave, Cambridge, MA 02139, USA
3240 Everyone is permitted to copy and distribute verbatim copies
3241 of this license document, but changing it is not allowed.
3245 The licenses for most software are designed to take away your
3246 freedom to share and change it. By contrast, the GNU General Public
3247 License is intended to guarantee your freedom to share and change free
3248 software--to make sure the software is free for all its users. This
3249 General Public License applies to most of the Free Software
3250 Foundation's software and to any other program whose authors commit to
3251 using it. (Some other Free Software Foundation software is covered by
3252 the GNU Library General Public License instead.) You can apply it to
3255 When we speak of free software, we are referring to freedom, not
3256 price. Our General Public Licenses are designed to make sure that you
3257 have the freedom to distribute copies of free software (and charge for
3258 this service if you wish), that you receive source code or can get it
3259 if you want it, that you can change the software or use pieces of it
3260 in new free programs; and that you know you can do these things.
3262 To protect your rights, we need to make restrictions that forbid
3263 anyone to deny you these rights or to ask you to surrender the rights.
3264 These restrictions translate to certain responsibilities for you if you
3265 distribute copies of the software, or if you modify it.
3267 For example, if you distribute copies of such a program, whether
3268 gratis or for a fee, you must give the recipients all the rights that
3269 you have. You must make sure that they, too, receive or can get the
3270 source code. And you must show them these terms so they know their
3273 We protect your rights with two steps: (1) copyright the software, and
3274 (2) offer you this license which gives you legal permission to copy,
3275 distribute and/or modify the software.
3277 Also, for each author's protection and ours, we want to make certain
3278 that everyone understands that there is no warranty for this free
3279 software. If the software is modified by someone else and passed on, we
3280 want its recipients to know that what they have is not the original, so
3281 that any problems introduced by others will not reflect on the original
3282 authors' reputations.
3284 Finally, any free program is threatened constantly by software
3285 patents. We wish to avoid the danger that redistributors of a free
3286 program will individually obtain patent licenses, in effect making the
3287 program proprietary. To prevent this, we have made it clear that any
3288 patent must be licensed for everyone's free use or not licensed at all.
3290 The precise terms and conditions for copying, distribution and
3291 modification follow.
3294 GNU GENERAL PUBLIC LICENSE
3295 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
3297 0. This License applies to any program or other work which contains
3298 a notice placed by the copyright holder saying it may be distributed
3299 under the terms of this General Public License. The "Program", below,
3300 refers to any such program or work, and a "work based on the Program"
3301 means either the Program or any derivative work under copyright law:
3302 that is to say, a work containing the Program or a portion of it,
3303 either verbatim or with modifications and/or translated into another
3304 language. (Hereinafter, translation is included without limitation in
3305 the term "modification".) Each licensee is addressed as "you".
3307 Activities other than copying, distribution and modification are not
3308 covered by this License; they are outside its scope. The act of
3309 running the Program is not restricted, and the output from the Program
3310 is covered only if its contents constitute a work based on the
3311 Program (independent of having been made by running the Program).
3312 Whether that is true depends on what the Program does.
3314 1. You may copy and distribute verbatim copies of the Program's
3315 source code as you receive it, in any medium, provided that you
3316 conspicuously and appropriately publish on each copy an appropriate
3317 copyright notice and disclaimer of warranty; keep intact all the
3318 notices that refer to this License and to the absence of any warranty;
3319 and give any other recipients of the Program a copy of this License
3320 along with the Program.
3322 You may charge a fee for the physical act of transferring a copy, and
3323 you may at your option offer warranty protection in exchange for a fee.
3325 2. You may modify your copy or copies of the Program or any portion
3326 of it, thus forming a work based on the Program, and copy and
3327 distribute such modifications or work under the terms of Section 1
3328 above, provided that you also meet all of these conditions:
3330 a) You must cause the modified files to carry prominent notices
3331 stating that you changed the files and the date of any change.
3333 b) You must cause any work that you distribute or publish, that in
3334 whole or in part contains or is derived from the Program or any
3335 part thereof, to be licensed as a whole at no charge to all third
3336 parties under the terms of this License.
3338 c) If the modified program normally reads commands interactively
3339 when run, you must cause it, when started running for such
3340 interactive use in the most ordinary way, to print or display an
3341 announcement including an appropriate copyright notice and a
3342 notice that there is no warranty (or else, saying that you provide
3343 a warranty) and that users may redistribute the program under
3344 these conditions, and telling the user how to view a copy of this
3345 License. (Exception: if the Program itself is interactive but
3346 does not normally print such an announcement, your work based on
3347 the Program is not required to print an announcement.)
3350 These requirements apply to the modified work as a whole. If
3351 identifiable sections of that work are not derived from the Program,
3352 and can be reasonably considered independent and separate works in
3353 themselves, then this License, and its terms, do not apply to those
3354 sections when you distribute them as separate works. But when you
3355 distribute the same sections as part of a whole which is a work based
3356 on the Program, the distribution of the whole must be on the terms of
3357 this License, whose permissions for other licensees extend to the
3358 entire whole, and thus to each and every part regardless of who wrote it.
3360 Thus, it is not the intent of this section to claim rights or contest
3361 your rights to work written entirely by you; rather, the intent is to
3362 exercise the right to control the distribution of derivative or
3363 collective works based on the Program.
3365 In addition, mere aggregation of another work not based on the Program
3366 with the Program (or with a work based on the Program) on a volume of
3367 a storage or distribution medium does not bring the other work under
3368 the scope of this License.
3370 3. You may copy and distribute the Program (or a work based on it,
3371 under Section 2) in object code or executable form under the terms of
3372 Sections 1 and 2 above provided that you also do one of the following:
3374 a) Accompany it with the complete corresponding machine-readable
3375 source code, which must be distributed under the terms of Sections
3376 1 and 2 above on a medium customarily used for software interchange; or,
3378 b) Accompany it with a written offer, valid for at least three
3379 years, to give any third party, for a charge no more than your
3380 cost of physically performing source distribution, a complete
3381 machine-readable copy of the corresponding source code, to be
3382 distributed under the terms of Sections 1 and 2 above on a medium
3383 customarily used for software interchange; or,
3385 c) Accompany it with the information you received as to the offer
3386 to distribute corresponding source code. (This alternative is
3387 allowed only for noncommercial distribution and only if you
3388 received the program in object code or executable form with such
3389 an offer, in accord with Subsection b above.)
3391 The source code for a work means the preferred form of the work for
3392 making modifications to it. For an executable work, complete source
3393 code means all the source code for all modules it contains, plus any
3394 associated interface definition files, plus the scripts used to
3395 control compilation and installation of the executable. However, as a
3396 special exception, the source code distributed need not include
3397 anything that is normally distributed (in either source or binary
3398 form) with the major components (compiler, kernel, and so on) of the
3399 operating system on which the executable runs, unless that component
3400 itself accompanies the executable.
3402 If distribution of executable or object code is made by offering
3403 access to copy from a designated place, then offering equivalent
3404 access to copy the source code from the same place counts as
3405 distribution of the source code, even though third parties are not
3406 compelled to copy the source along with the object code.
3409 4. You may not copy, modify, sublicense, or distribute the Program
3410 except as expressly provided under this License. Any attempt
3411 otherwise to copy, modify, sublicense or distribute the Program is
3412 void, and will automatically terminate your rights under this License.
3413 However, parties who have received copies, or rights, from you under
3414 this License will not have their licenses terminated so long as such
3415 parties remain in full compliance.
3417 5. You are not required to accept this License, since you have not
3418 signed it. However, nothing else grants you permission to modify or
3419 distribute the Program or its derivative works. These actions are
3420 prohibited by law if you do not accept this License. Therefore, by
3421 modifying or distributing the Program (or any work based on the
3422 Program), you indicate your acceptance of this License to do so, and
3423 all its terms and conditions for copying, distributing or modifying
3424 the Program or works based on it.
3426 6. Each time you redistribute the Program (or any work based on the
3427 Program), the recipient automatically receives a license from the
3428 original licensor to copy, distribute or modify the Program subject to
3429 these terms and conditions. You may not impose any further
3430 restrictions on the recipients' exercise of the rights granted herein.
3431 You are not responsible for enforcing compliance by third parties to
3434 7. If, as a consequence of a court judgment or allegation of patent
3435 infringement or for any other reason (not limited to patent issues),
3436 conditions are imposed on you (whether by court order, agreement or
3437 otherwise) that contradict the conditions of this License, they do not
3438 excuse you from the conditions of this License. If you cannot
3439 distribute so as to satisfy simultaneously your obligations under this
3440 License and any other pertinent obligations, then as a consequence you
3441 may not distribute the Program at all. For example, if a patent
3442 license would not permit royalty-free redistribution of the Program by
3443 all those who receive copies directly or indirectly through you, then
3444 the only way you could satisfy both it and this License would be to
3445 refrain entirely from distribution of the Program.
3447 If any portion of this section is held invalid or unenforceable under
3448 any particular circumstance, the balance of the section is intended to
3449 apply and the section as a whole is intended to apply in other
3452 It is not the purpose of this section to induce you to infringe any
3453 patents or other property right claims or to contest validity of any
3454 such claims; this section has the sole purpose of protecting the
3455 integrity of the free software distribution system, which is
3456 implemented by public license practices. Many people have made
3457 generous contributions to the wide range of software distributed
3458 through that system in reliance on consistent application of that
3459 system; it is up to the author/donor to decide if he or she is willing
3460 to distribute software through any other system and a licensee cannot
3463 This section is intended to make thoroughly clear what is believed to
3464 be a consequence of the rest of this License.
3467 8. If the distribution and/or use of the Program is restricted in
3468 certain countries either by patents or by copyrighted interfaces, the
3469 original copyright holder who places the Program under this License
3470 may add an explicit geographical distribution limitation excluding
3471 those countries, so that distribution is permitted only in or among
3472 countries not thus excluded. In such case, this License incorporates
3473 the limitation as if written in the body of this License.
3475 9. The Free Software Foundation may publish revised and/or new versions
3476 of the General Public License from time to time. Such new versions will
3477 be similar in spirit to the present version, but may differ in detail to
3478 address new problems or concerns.
3480 Each version is given a distinguishing version number. If the Program
3481 specifies a version number of this License which applies to it and "any
3482 later version", you have the option of following the terms and conditions
3483 either of that version or of any later version published by the Free
3484 Software Foundation. If the Program does not specify a version number of
3485 this License, you may choose any version ever published by the Free Software
3488 10. If you wish to incorporate parts of the Program into other free
3489 programs whose distribution conditions are different, write to the author
3490 to ask for permission. For software which is copyrighted by the Free
3491 Software Foundation, write to the Free Software Foundation; we sometimes
3492 make exceptions for this. Our decision will be guided by the two goals
3493 of preserving the free status of all derivatives of our free software and
3494 of promoting the sharing and reuse of software generally.
3498 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
3499 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
3500 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
3501 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
3502 OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
3503 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
3504 TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
3505 PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
3506 REPAIR OR CORRECTION.
3508 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
3509 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
3510 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
3511 INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
3512 OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
3513 TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
3514 YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
3515 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
3516 POSSIBILITY OF SUCH DAMAGES.
3520 def create_menu_and_toolbar
3523 mb = Gtk::MenuBar.new
3525 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
3526 filesubmenu = Gtk::Menu.new
3527 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
3528 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
3529 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3530 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
3531 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
3532 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3533 tooltips = Gtk::Tooltips.new
3534 filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
3535 $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3536 tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
3537 filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
3538 $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3539 tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
3540 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
3541 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3542 tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums (anywhere)")), nil)
3543 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3544 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
3545 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3546 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
3547 filesubmenu.append($view_wa = Gtk::ImageMenuItem.new(utf8(_("View web-album with browser"))).set_sensitive(false))
3548 $view_wa.image = Gtk::Image.new("#{$FPATH}/images/stock-view-webalbum-16.png")
3549 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3550 filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
3551 tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
3552 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3553 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
3554 filemenu.set_submenu(filesubmenu)
3557 new.signal_connect('activate') { new_album }
3558 open.signal_connect('activate') { open_file_popup }
3559 $save.signal_connect('activate') { save_current_file_user }
3560 $save_as.signal_connect('activate') { save_as_do }
3561 $merge_current.signal_connect('activate') { merge_current }
3562 $merge_newsubs.signal_connect('activate') { merge_newsubs }
3563 $merge.signal_connect('activate') { merge }
3564 $generate.signal_connect('activate') {
3566 call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
3567 utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
3569 { :successmsg => utf8(_("Your web-album is now ready in directory '%s'.
3570 Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
3571 :successmsg_linkurl => $xmldoc.root.attributes['destination'],
3572 :closure_after => proc {
3573 $xmldoc.elements.each('//dir') { |elem|
3574 $modified ||= elem.attributes['already-generated'].nil?
3575 elem.add_attribute('already-generated', 'true')
3577 UndoHandler.cleanup #- prevent save_changes to mark current dir as not already generated
3578 $undo_tb.sensitive = $undo_mb.sensitive = false
3579 $redo_tb.sensitive = $redo_mb.sensitive = false
3581 $generated_outofline = true
3584 $view_wa.signal_connect('activate') {
3585 indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
3586 if File.exists?(indexhtml)
3589 show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
3592 $properties.signal_connect('activate') { properties }
3594 quit.signal_connect('activate') { try_quit }
3596 editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
3597 editsubmenu = Gtk::Menu.new
3598 editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
3599 editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
3600 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3601 editsubmenu.append($sort_by_exif_date = Gtk::ImageMenuItem.new(utf8(_("Sort by EXIF date"))).set_sensitive(false))
3602 $sort_by_exif_date.image = Gtk::Image.new("#{$FPATH}/images/sort_by_exif_date.png")
3603 editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
3604 $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
3605 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)
3606 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3607 editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
3608 editmenu.set_submenu(editsubmenu)
3611 $remove_all_captions.signal_connect('activate') { remove_all_captions }
3612 $sort_by_exif_date.signal_connect('activate') { sort_by_exif_date }
3614 prefs.signal_connect('activate') { preferences }
3616 helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
3617 helpsubmenu = Gtk::Menu.new
3618 helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
3619 one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
3620 helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
3621 speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
3622 helpsubmenu.append(tutos = Gtk::ImageMenuItem.new(utf8(_("Online tutorials (opens a web-browser)"))))
3623 tutos.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3624 helpsubmenu.append( Gtk::SeparatorMenuItem.new)
3625 helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
3626 helpmenu.set_submenu(helpsubmenu)
3629 one_click.signal_connect('activate') {
3630 show_one_click_explanation(_("One-Click tools are available in the toolbar."))
3633 speed.signal_connect('activate') {
3634 show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
3636 <span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
3637 <span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
3638 <span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
3639 <span foreground='darkblue'>Control-Enter</span>: for an image, open larger view; for a video, launch player
3640 <span foreground='darkblue'>Control-Delete</span>: delete image
3641 <span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
3642 <span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
3643 <span foreground='darkblue'>Control-z</span>: undo
3644 <span foreground='darkblue'>Control-r</span>: redo
3646 <span size='large' weight='bold'>Mouse gestures:</span>
3648 Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
3649 for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
3651 <span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
3652 <span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
3653 <span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
3654 <span foreground='darkblue'>Left click, hold left button, right click</span>: undo
3655 <span foreground='darkblue'>Right click, hold right button, left click</span>: redo
3656 ")), { :pos_centered => true, :not_transient => true })
3659 tutos.signal_connect('activate') {
3660 open_url('http://zarb.org/~gc/html/booh/tutorial.html')
3663 about.signal_connect('activate') {
3664 Gtk::AboutDialog.set_url_hook { |dialog, url| open_url(url) }
3665 Gtk::AboutDialog.show($main_window, { :name => 'booh',
3666 :version => $VERSION,
3667 :copyright => 'Copyright (c) 2005 Guillaume Cottenceau',
3668 :license => get_license,
3669 :website => 'http://zarb.org/~gc/html/booh.html',
3670 :authors => [ 'Guillaume Cottenceau' ],
3671 :artists => [ 'Ayo73' ],
3672 :comments => utf8(_("''The Web-Album of choice for discriminating Linux users''")),
3673 :translator_credits => utf8(_('Japanese: Masao Mutoh
3674 German: Roland Eckert
3675 French: Guillaume Cottenceau')),
3676 :logo => Gdk::Pixbuf.new("#{$FPATH}/images/logo.png") })
3681 tb = Gtk::Toolbar.new
3683 tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
3684 open.label = utf8(_("Open")) #- to avoid missing gtk2 l10n catalogs
3685 open.menu = Gtk::Menu.new
3686 open.signal_connect('clicked') { open_file_popup }
3687 open.signal_connect('show-menu') {
3688 lastopens = Gtk::Menu.new
3690 if $config['last-opens']
3691 $config['last-opens'].reverse.each { |e|
3692 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
3693 item.signal_connect('activate') {
3694 if ask_save_modifications(utf8(_("Save this album?")),
3695 utf8(_("Do you want to save the changes to this album?")),
3696 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3697 push_mousecursor_wait
3698 msg = open_file_user(from_utf8(e))
3701 show_popup($main_window, msg)
3709 open.menu = lastopens
3712 tb.insert(-1, Gtk::SeparatorToolItem.new)
3714 tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
3715 $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
3716 $r90.label = utf8(_("Rotate"))
3717 tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
3718 $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
3719 $r270.label = utf8(_("Rotate"))
3720 tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
3721 $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
3722 $enhance.label = utf8(_("Enhance"))
3723 tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
3724 $delete.label = utf8(_("Delete")) #- to avoid missing gtk2 l10n catalogs
3725 tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
3726 nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
3727 nothing.label = utf8(_("None"))
3729 tb.insert(-1, Gtk::SeparatorToolItem.new)
3731 tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
3732 tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
3735 $undo_tb.signal_connect('clicked') { perform_undo }
3736 $undo_mb.signal_connect('activate') { perform_undo }
3737 $redo_tb.signal_connect('clicked') { perform_redo }
3738 $redo_mb.signal_connect('activate') { perform_redo }
3740 one_click_explain_try = proc {
3741 if !$config['one-click-explained']
3742 show_one_click_explanation(_("You have just clicked on a One-Click tool."))
3743 $config['one-click-explained'] = true
3747 $r90.signal_connect('toggled') {
3749 set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
3750 one_click_explain_try.call
3751 $r270.active = false
3752 $enhance.active = false
3753 $delete.active = false
3754 nothing.sensitive = true
3756 if !$r270.active? && !$enhance.active? && !$delete.active?
3757 set_mousecursor_normal
3758 nothing.sensitive = false
3760 nothing.sensitive = true
3764 $r270.signal_connect('toggled') {
3766 set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
3767 one_click_explain_try.call
3769 $enhance.active = false
3770 $delete.active = false
3771 nothing.sensitive = true
3773 if !$r90.active? && !$enhance.active? && !$delete.active?
3774 set_mousecursor_normal
3775 nothing.sensitive = false
3777 nothing.sensitive = true
3781 $enhance.signal_connect('toggled') {
3783 set_mousecursor(Gdk::Cursor::SPRAYCAN)
3784 one_click_explain_try.call
3786 $r270.active = false
3787 $delete.active = false
3788 nothing.sensitive = true
3790 if !$r90.active? && !$r270.active? && !$delete.active?
3791 set_mousecursor_normal
3792 nothing.sensitive = false
3794 nothing.sensitive = true
3798 $delete.signal_connect('toggled') {
3800 set_mousecursor(Gdk::Cursor::PIRATE)
3801 one_click_explain_try.call
3803 $r270.active = false
3804 $enhance.active = false
3805 nothing.sensitive = true
3807 if !$r90.active? && !$r270.active? && !$enhance.active?
3808 set_mousecursor_normal
3809 nothing.sensitive = false
3811 nothing.sensitive = true
3815 nothing.signal_connect('clicked') {
3816 $r90.active = $r270.active = $enhance.active = $delete.active = false
3817 set_mousecursor_normal
3823 def gtk_thread_protect(&proc)
3824 if Thread.current == Thread.main
3827 $protect_gtk_pending_calls.synchronize {
3828 $gtk_pending_calls << proc
3833 def gtk_thread_flush
3834 $protect_gtk_pending_calls.try_lock
3835 for closure in $gtk_pending_calls
3838 $gtk_pending_calls = []
3839 $protect_gtk_pending_calls.unlock
3842 def ask_password_protect
3843 value = $xmldir.attributes['password-protect']
3845 dialog = Gtk::Dialog.new(utf8(_("Password protect this sub-album")),
3847 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3848 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3849 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3851 lbl = Gtk::Label.new
3853 _("You can choose to <b>password protect</b> the sub-album '%s' (only available
3854 if you plan to publish your web-album with an Apache web-server). This will use
3855 the .htaccess/.htpasswd feature of Apache (not so strongly crypted password, but
3856 generally ok for protecting web contents). Users will be prompted with a dialog
3857 asking for a username and a password, failure to give the correct pair will
3859 ") % File.basename($current_path))
3860 dialog.vbox.add(lbl)
3861 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("free access")))).
3862 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("password protect with password file:")))).
3863 add(file = Gtk::Entry.new)))
3864 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0.5, 0.2).add(Gtk::HBox.new.add(bt_help = Gtk::Button.new(utf8(_("help about password file")))).
3865 add(Gtk::Label.new).
3866 add(bt_gen = Gtk::Button.new(utf8(_("generate a password file"))))))
3867 dialog.window_position = Gtk::Window::POS_MOUSE
3872 rb_yes.active = true
3876 bt_help.signal_connect('clicked') {
3877 show_popup(dialog, utf8(
3878 _("Password protection proposed here uses the .htaccess/.htpasswd features
3879 proposed by Apache. So first, be sure you will publish your web-album on an
3880 Apache web-server. Second, you will need to have a .htpasswd file accessible
3881 by Apache somewhere on the web-server disks. The password file you must
3882 provide in the dialog when choosing to password protect is the full absolute
3883 path to access this file <b>on the web-server</b> (not on your machine). Note
3884 that if you use a relative path, it will be considered relative to the
3885 Document Root of the Apache configuration.")))
3888 bt_gen.signal_connect('clicked') {
3889 gendialog = Gtk::Dialog.new(utf8(_("Generate a password file")),
3891 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3892 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3893 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3895 lbl = Gtk::Label.new
3897 _("I can generate a password file (.htpasswd for Apache) for you. Just type
3898 the username and password you wish to put in it below and validate."))
3899 gendialog.vbox.add(lbl)
3900 gendialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(Gtk::Label.new(utf8(_('Username:')))).
3901 add(user = Gtk::Entry.new).
3902 add(Gtk::Label.new(utf8(_('Password:')))).
3903 add(pass = Gtk::Entry.new)))
3904 pass.visibility = false
3905 gendialog.window_position = Gtk::Window::POS_MOUSE
3907 gendialog.run { |response|
3911 if response == Gtk::Dialog::RESPONSE_OK
3913 ary = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a + [ '.', '/' ]
3914 return ary[rand(ary.length)]
3916 fout = Tempfile.new("htpasswd")
3917 fout.write("#{u}:#{p.crypt(rand_letter + rand_letter)}\n")
3919 File.chmod(0644, fout.path)
3920 show_popup(dialog, utf8(
3921 _("The file <b>%s</b> now contains the username and the crypted password. Now
3922 copy it to a suitable location on the machine hosting the Apache web-server (better not
3923 below the Document Root), and specify this location in the password protect dialog.") % fout.path), { :selectable => true })
3928 dialog.run { |response|
3935 if response == Gtk::Dialog::RESPONSE_OK && value != newval
3937 msg 3, "changing password protection of #{$current_path} to #{newval}"
3939 $xmldir.delete_attribute('password-protect')
3941 $xmldir.add_attribute('password-protect', newval)
3943 save_undo(_("set password protection for %s") % File.basename($current_path),
3946 $xmldir.delete_attribute('password-protect')
3948 $xmldir.add_attribute('password-protect', value)
3952 $xmldir.delete_attribute('password-protect')
3954 $xmldir.add_attribute('password-protect', newval)
3958 show_password_protections
3963 def create_main_window
3965 mb, tb = create_menu_and_toolbar
3967 $albums_tv = Gtk::TreeView.new
3968 $albums_tv.set_size_request(120, -1)
3969 $albums_tv.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }))
3970 $albums_tv.append_column(tcol = Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, { :text => 0 }))
3971 $albums_tv.expander_column = tcol
3972 $albums_tv.set_headers_visible(false)
3973 $albums_tv.selection.signal_connect('changed') { |w|
3974 push_mousecursor_wait
3978 msg 3, "no selection"
3980 $current_path = $albums_ts.get_value(iter, 1)
3985 $albums_tv.signal_connect('button-release-event') { |w, event|
3986 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3 && !$current_path.nil?
3987 menu = Gtk::Menu.new
3988 menu.append(passprotect = Gtk::ImageMenuItem.new(utf8(_("Password protect"))))
3989 passprotect.image = Gtk::Image.new("#{$FPATH}/images/galeon-secure.png")
3990 passprotect.signal_connect('activate') { ask_password_protect }
3991 menu.append(restore = Gtk::ImageMenuItem.new(utf8(_("Restore deleted images/videos/subalbums"))))
3992 restore.image = Gtk::Image.new("#{$FPATH}/images/restore.png")
3993 restore.signal_connect('activate') { restore_deleted }
3994 menu.append(Gtk::SeparatorMenuItem.new)
3995 menu.append(delete = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
3996 delete.signal_connect('activate') {
3997 if show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed? This operation cannot be undone.")), { :okcancel => true })
3998 delete_current_subalbum
4002 menu.popup(nil, nil, event.button, event.time)
4006 $albums_ts = Gtk::TreeStore.new(String, String, Gdk::Pixbuf)
4007 $albums_tv.set_model($albums_ts)
4008 $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
4010 albums_sw = Gtk::ScrolledWindow.new(nil, nil)
4011 albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
4012 albums_sw.add_with_viewport($albums_tv)
4014 $notebook = Gtk::Notebook.new
4015 create_subalbums_page
4016 $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
4018 $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
4020 $notebook.signal_connect('switch-page') { |w, page, num|
4022 $delete.active = false
4023 $delete.sensitive = false
4025 $delete.sensitive = true
4027 if $xmldir && $subalbums_edits[$xmldir.attributes['path']] && textview = $subalbums_edits[$xmldir.attributes['path']][:editzone]
4029 textview.buffer.text = $thumbnails_title.buffer.text
4031 if $notebook.get_tab_label($autotable_sw).sensitive?
4032 $thumbnails_title.buffer.text = textview.buffer.text
4038 paned = Gtk::HPaned.new
4039 paned.pack1(albums_sw, false, false)
4040 paned.pack2($notebook, true, true)
4042 main_vbox = Gtk::VBox.new(false, 0)
4043 main_vbox.pack_start(mb, false, false)
4044 main_vbox.pack_start(tb, false, false)
4045 main_vbox.pack_start(paned, true, true)
4046 main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
4048 $main_window = Gtk::Window.new
4049 $main_window.add(main_vbox)
4050 $main_window.signal_connect('delete-event') {
4051 try_quit({ :disallow_cancel => true })
4054 #- read/save size and position of window
4055 if $config['pos-x'] && $config['pos-y']
4056 $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
4058 $main_window.window_position = Gtk::Window::POS_CENTER
4060 msg 3, "size: #{$config['width']}x#{$config['height']}"
4061 $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
4062 $main_window.signal_connect('configure-event') {
4063 msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
4064 x, y = $main_window.window.root_origin
4065 width, height = $main_window.window.size
4066 $config['pos-x'] = x
4067 $config['pos-y'] = y
4068 $config['width'] = width
4069 $config['height'] = height
4073 $protect_gtk_pending_calls = Mutex.new
4074 $gtk_pending_calls = []
4075 Gtk.timeout_add(100) {
4078 $protect_gtk_pending_calls.synchronize {
4079 if ! $gtk_pending_calls.empty?
4080 $gtk_pending_calls.shift.call
4082 empty = $gtk_pending_calls.empty?
4088 $statusbar.push(0, utf8(_("Ready.")))
4089 $main_window.show_all
4092 Thread.abort_on_exception = true
4102 open_file_user(ARGV[0])