5 # A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
13 # Copyright (c) 2004 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
27 require 'booh/gtkadds'
28 require 'booh/GtkAutoTable'
32 bindtextdomain("booh")
34 require 'rexml/document'
37 require 'booh/booh-lib'
39 require 'booh/UndoHandler'
44 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
46 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
49 #- default values for some globals
53 $ignore_videos = false
54 $button1_pressed_autotable = false
55 $generated_outofline = false
58 puts _("Usage: %s [OPTION]...") % File.basename($0)
60 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
65 parser = GetoptLong.new
66 parser.set_options(*$options.collect { |ary| ary[0..2] })
68 parser.each_option do |name, arg|
74 when '--verbose-level'
75 $verbose_level = arg.to_i
88 $config_file = File.expand_path('~/.booh-gui-rc')
89 if File.readable?($config_file)
90 $xmldoc = REXML::Document.new(File.new($config_file))
91 $xmldoc.root.elements.each { |element|
92 txt = element.get_text
94 if txt.value =~ /~~~/ || element.name == 'last-opens'
95 $config[element.name] = txt.value.split(/~~~/)
97 $config[element.name] = txt.value
99 elsif element.elements.size == 0
100 $config[element.name] = ''
102 $config[element.name] = {}
103 element.each { |chld|
105 $config[element.name][chld.name] = txt ? txt.value : nil
110 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
111 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
112 $config['comments-format'] ||= '%t'
113 if !FileTest.directory?(File.expand_path('~/.booh'))
114 system("mkdir ~/.booh")
122 if !system("which convert >/dev/null 2>/dev/null")
123 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
124 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
127 if !system("which identify >/dev/null 2>/dev/null")
128 show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
129 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
131 missing = %w(transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
133 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
136 viewer_binary = $config['video-viewer'].split.first
137 if viewer_binary && !File.executable?(viewer_binary)
138 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
139 You should fix this in Edit/Preferences so that you can view videos.
141 Problem was: '%s' is not an executable file.
142 Hint: don't forget to specify the full path to the executable,
143 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
145 browser_binary = $config['browser'].split.first
146 if browser_binary && !File.executable?(browser_binary)
147 show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
148 You should fix this in Edit/Preferences so that you can open URLs.
150 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
155 if $config['last-opens'] && $config['last-opens'].size > 10
156 $config['last-opens'] = $config['last-opens'][-10, 10]
159 ios = File.open($config_file, "w")
160 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
161 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
162 $config.each_pair { |key, value|
163 elem = $xmldoc.root.add_element key
165 $config[key].each_pair { |subkey, subvalue|
166 subelem = elem.add_element subkey
167 subelem.add_text subvalue.to_s
169 elsif value.is_a? Array
170 elem.add_text value.join('~~~')
175 elem.add_text value.to_s
179 $xmldoc.write(ios, 0)
182 $tempfiles.each { |f|
187 def set_mousecursor(what, *widget)
188 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
189 if widget[0] && widget[0].window
190 widget[0].window.cursor = cursor
192 if $main_window && $main_window.window
193 $main_window.window.cursor = cursor
195 $current_cursor = what
197 def set_mousecursor_wait(*widget)
198 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
199 if Thread.current == Thread.main
200 Gtk.main_iteration while Gtk.events_pending?
203 def set_mousecursor_normal(*widget)
204 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
206 def push_mousecursor_wait(*widget)
207 if $current_cursor != Gdk::Cursor::WATCH
208 $save_cursor = $current_cursor
209 gtk_thread_protect { set_mousecursor_wait(*widget) }
212 def pop_mousecursor(*widget)
213 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
217 source = $xmldoc.root.attributes['source']
218 dest = $xmldoc.root.attributes['destination']
219 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
222 def full_src_dir_to_rel(path, source)
223 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
226 def build_full_dest_filename(filename)
227 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
230 def save_undo(name, closure, *params)
231 UndoHandler.save_undo(name, closure, [ *params ])
232 $undo_tb.sensitive = $undo_mb.sensitive = true
233 $redo_tb.sensitive = $redo_mb.sensitive = false
236 def view_element(filename, closures)
237 if entry2type(filename) == 'video'
238 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
244 w = Gtk::Window.new.set_title(filename)
246 msg 3, "filename: #{filename}"
247 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
248 #- typically this file won't exist in case of videos; try with the largest thumbnail around
249 if !File.exists?(dest_img)
250 if entry2type(filename) == 'video'
251 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
252 if not alternatives.empty?
253 dest_img = alternatives[-1]
256 push_mousecursor_wait
257 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
259 if !File.exists?(dest_img)
260 msg 2, _("Could not generate fullscreen thumbnail!")
265 evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(Gtk::Image.new(dest_img)).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
266 evt.signal_connect('button-press-event') { |this, event|
267 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
268 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
270 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
272 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
273 delete_item.signal_connect('activate') {
275 closures[:delete].call
278 menu.popup(nil, nil, event.button, event.time)
281 evt.signal_connect('button-release-event') { |this, event|
283 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
284 msg 3, "gesture delete: click-drag right button to the bottom"
286 closures[:delete].call
287 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
291 tooltips = Gtk::Tooltips.new
292 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
294 w.signal_connect('key-press-event') { |w,event|
295 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
297 closures[:delete].call
301 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
302 b.signal_connect('clicked') { w.destroy }
305 vb.pack_start(evt, false, false)
306 vb.pack_end(bottom, false, false)
309 w.signal_connect('delete-event') { w.destroy }
310 w.window_position = Gtk::Window::POS_CENTER
314 def scroll_upper(scrolledwindow, ypos_top)
315 newval = scrolledwindow.vadjustment.value -
316 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
317 if newval < scrolledwindow.vadjustment.lower
318 newval = scrolledwindow.vadjustment.lower
320 scrolledwindow.vadjustment.value = newval
323 def scroll_lower(scrolledwindow, ypos_bottom)
324 newval = scrolledwindow.vadjustment.value +
325 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
326 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
327 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
329 scrolledwindow.vadjustment.value = newval
332 def autoscroll_if_needed(scrolledwindow, image, textview)
333 #- autoscroll if cursor or image is not visible, if possible
334 if image && image.window || textview.window
335 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
336 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
337 current_miny_visible = scrolledwindow.vadjustment.value
338 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
339 if ypos_top < current_miny_visible
340 scroll_upper(scrolledwindow, ypos_top)
341 elsif ypos_bottom > current_maxy_visible
342 scroll_lower(scrolledwindow, ypos_bottom)
347 def create_editzone(scrolledwindow, pagenum, image)
348 frame = Gtk::Frame.new
349 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
350 frame.set_shadow_type(Gtk::SHADOW_IN)
351 textview.signal_connect('key-press-event') { |w, event|
352 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
353 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
354 scrolledwindow.signal_emit('key-press-event', event)
356 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
357 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
358 if event.keyval == Gdk::Keyval::GDK_Up
359 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
360 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
362 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
365 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
366 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
368 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
375 candidate_undo_text = nil
376 textview.signal_connect('focus-in-event') { |w, event|
377 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
378 candidate_undo_text = textview.buffer.text
382 textview.signal_connect('key-release-event') { |w, event|
383 if candidate_undo_text && candidate_undo_text != textview.buffer.text
385 save_undo(_("text edit"),
387 save_text = textview.buffer.text
388 textview.buffer.text = text
390 $notebook.set_page(pagenum)
392 textview.buffer.text = save_text
394 $notebook.set_page(pagenum)
396 }, candidate_undo_text)
397 candidate_undo_text = nil
400 if event.state != 0 || ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
401 autoscroll_if_needed(scrolledwindow, image, textview)
406 return [ frame, textview ]
409 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
411 if !$modified_pixbufs[thumbnail_img]
412 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
413 elsif !$modified_pixbufs[thumbnail_img][:orig]
414 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
417 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
420 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
421 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
422 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
423 if pixbuf.height > desired_y
424 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
425 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
426 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
431 if $modified_pixbufs[thumbnail_img][:whitebalance]
432 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
435 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
438 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
441 #- update rotate attribute
442 xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
444 $modified_pixbufs[thumbnail_img] ||= {}
445 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
446 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
448 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
451 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
454 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
456 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
458 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
459 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
461 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
462 $notebook.set_page(0)
463 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
468 def color_swap(xmldir, attributes_prefix)
470 if xmldir.attributes["#{attributes_prefix}color-swap"]
471 xmldir.delete_attribute("#{attributes_prefix}color-swap")
473 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
477 def enhance(xmldir, attributes_prefix)
479 if xmldir.attributes["#{attributes_prefix}enhance"]
480 xmldir.delete_attribute("#{attributes_prefix}enhance")
482 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
486 def change_frame_offset(xmldir, attributes_prefix, value)
488 xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
491 def ask_new_frame_offset(xmldir, attributes_prefix)
493 value = xmldir.attributes["#{attributes_prefix}frame-offset"]
498 dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
500 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
501 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
502 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
506 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
507 from. There are approximately 25 frames per second in a video.
510 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
511 entry.signal_connect('key-press-event') { |w, event|
512 if event.keyval == Gdk::Keyval::GDK_Return
513 dialog.response(Gtk::Dialog::RESPONSE_OK)
515 elsif event.keyval == Gdk::Keyval::GDK_Escape
516 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
519 false #- propagate if needed
523 dialog.window_position = Gtk::Window::POS_MOUSE
526 dialog.run { |response|
529 if response == Gtk::Dialog::RESPONSE_OK
531 msg 3, "changing frame offset to #{newval}"
532 return { :old => value, :new => newval }
539 def change_pano_amount(xmldir, attributes_prefix, value)
542 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
544 xmldir.add_attribute("#{attributes_prefix}pano-amount", value)
548 def ask_new_pano_amount(xmldir, attributes_prefix)
550 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
555 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
557 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
558 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
559 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
563 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
564 of this panorama image compared to other regular images. For example, if the panorama
565 was taken out of four photos on one row, counting the necessary overlap, the width of
566 this panorama image should probably be roughly three times the width of regular images.
568 With this information, booh will be able to generate panorama thumbnails looking
572 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("none (not a panorama image)")))).
573 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
574 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
575 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
576 dialog.window_position = Gtk::Window::POS_MOUSE
579 spin.value = value.to_f
586 dialog.run { |response|
590 newval = spin.value.to_f
593 if response == Gtk::Dialog::RESPONSE_OK
595 msg 3, "changing panorama amount to #{newval}"
596 return { :old => value, :new => newval }
603 def change_whitebalance(xmlelem, attributes_prefix, value)
605 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
608 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
610 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
611 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
612 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
613 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
614 destfile = "#{thumbnail_img}-orig-whitebalance.jpg"
615 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
616 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
617 $modified_pixbufs[thumbnail_img] ||= {}
618 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
619 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
620 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
623 $modified_pixbufs[thumbnail_img] ||= {}
624 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
626 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
629 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
630 #- init $modified_pixbufs correctly
631 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
633 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
635 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
637 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
638 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
639 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
643 _("You can fix the <b>white balance</b> of the image, if your image is too blue
644 or too yellow because your camera didn't detect the light correctly. Drag the
645 slider below the image to the left for more blue, to the right for more yellow.
649 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
651 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
653 dialog.window_position = Gtk::Window::POS_MOUSE
657 timeout = Gtk.timeout_add(100) {
658 if hs.value != lastval
661 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
667 dialog.run { |response|
668 Gtk.timeout_remove(timeout)
669 if response == Gtk::Dialog::RESPONSE_OK
671 newval = hs.value.to_s
672 msg 3, "changing white balance to #{newval}"
674 return { :old => value, :new => newval }
677 $modified_pixbufs[thumbnail_img] ||= {}
678 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
679 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
687 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
688 system("rm -f '#{destfile}'")
689 #- type can be 'element' or 'subdir'
691 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
693 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
697 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
699 push_mousecursor_wait
700 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
703 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
709 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
710 distribute_multiple_call = Proc.new { |action, arg|
711 $selected_elements.each_key { |path|
712 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
714 if possible_actions[:can_multiple] && $selected_elements.length > 0
715 UndoHandler.begin_batch
716 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
717 UndoHandler.end_batch
719 closures[action].call(arg)
721 $selected_elements = {}
724 if optionals.include?('change_image')
725 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
726 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
727 changeimg.signal_connect('activate') { closures[:change].call }
728 menu.append(Gtk::SeparatorMenuItem.new)
730 if !possible_actions[:can_multiple] || $selected_elements.length == 0
733 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
734 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
735 view.signal_connect('activate') { closures[:view].call }
737 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
738 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
739 view.signal_connect('activate') { closures[:view].call }
740 menu.append(Gtk::SeparatorMenuItem.new)
743 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
744 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
745 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
746 exif.signal_connect('activate') { show_popup($main_window,
747 utf8(`identify -format "%[EXIF:*]" #{fullpath}`.sub(/MakerNote.*\n/, '')),
748 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
749 menu.append(Gtk::SeparatorMenuItem.new)
752 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
753 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
754 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
755 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
756 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
757 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
758 if !possible_actions[:can_multiple] || $selected_elements.length == 0
759 menu.append(Gtk::SeparatorMenuItem.new)
760 if !possible_actions[:forbid_left]
761 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
762 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
763 moveleft.signal_connect('activate') { closures[:move].call('left') }
764 if !possible_actions[:can_left]
765 moveleft.sensitive = false
768 if !possible_actions[:forbid_right]
769 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
770 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
771 moveright.signal_connect('activate') { closures[:move].call('right') }
772 if !possible_actions[:can_right]
773 moveright.sensitive = false
776 if optionals.include?('move_top')
777 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
778 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
779 movetop.signal_connect('activate') { closures[:move].call('top') }
780 if !possible_actions[:can_top]
781 movetop.sensitive = false
784 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
785 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
786 moveup.signal_connect('activate') { closures[:move].call('up') }
787 if !possible_actions[:can_up]
788 moveup.sensitive = false
790 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
791 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
792 movedown.signal_connect('activate') { closures[:move].call('down') }
793 if !possible_actions[:can_down]
794 movedown.sensitive = false
796 if optionals.include?('move_bottom')
797 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
798 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
799 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
800 if !possible_actions[:can_bottom]
801 movebottom.sensitive = false
806 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
807 menu.append(Gtk::SeparatorMenuItem.new)
808 menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
809 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
810 color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
811 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
812 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
813 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
814 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
815 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
816 frame_offset.signal_connect('activate') {
817 if possible_actions[:can_multiple] && $selected_elements.length > 0
818 if values = ask_new_frame_offset(nil, '')
819 distribute_multiple_call.call(:frame_offset, values)
822 closures[:frame_offset].call
827 menu.append( Gtk::SeparatorMenuItem.new)
828 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
829 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
830 whitebalance.signal_connect('activate') {
831 if possible_actions[:can_multiple] && $selected_elements.length > 0
832 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
833 distribute_multiple_call.call(:whitebalance, values)
836 closures[:whitebalance].call
839 if !possible_actions[:can_multiple] || $selected_elements.length == 0
840 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
841 _("Enhance constrast"))))
843 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
845 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
846 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
847 if type == 'image' && possible_actions[:can_panorama]
848 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
849 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
850 panorama.signal_connect('activate') {
851 if possible_actions[:can_multiple] && $selected_elements.length > 0
852 if values = ask_new_pano_amount(nil, '')
853 distribute_multiple_call.call(:pano, values)
856 distribute_multiple_call.call(:pano)
860 if optionals.include?('delete')
861 menu.append( Gtk::SeparatorMenuItem.new)
862 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
863 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
864 if !possible_actions[:can_multiple] || $selected_elements.length == 0
865 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
866 paste_item.signal_connect('activate') { closures[:paste].call }
867 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
868 clear_item.signal_connect('activate') { $cuts = [] }
870 paste_item.sensitive = clear_item.sensitive = false
873 menu.append( Gtk::SeparatorMenuItem.new)
874 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
875 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
876 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
877 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
879 menu.append( Gtk::SeparatorMenuItem.new)
880 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
881 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
884 menu.popup(nil, nil, event.button, event.time)
887 def delete_current_subalbum
889 sel = $albums_tv.selection.selected_rows
890 $xmldir.elements.each { |e|
891 if e.name == 'image' || e.name == 'video'
892 e.add_attribute('deleted', 'true')
895 #- branch if we have a non deleted subalbum
896 if $xmldir.child_byname_notattr('dir', 'deleted')
897 $xmldir.delete_attribute('thumbnails-caption')
898 $xmldir.delete_attribute('thumbnails-captionfile')
900 $xmldir.add_attribute('deleted', 'true')
902 while moveup.parent.name == 'dir'
903 moveup = moveup.parent
904 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
905 moveup.add_attribute('deleted', 'true')
912 save_changes('forced')
913 populate_subalbums_treeview(false)
914 $albums_tv.selection.select_path(sel[0])
920 $current_path = nil #- prevent save_changes from being rerun again
921 sel = $albums_tv.selection.selected_rows
922 restore_one = proc { |xmldir|
923 xmldir.elements.each { |e|
924 if e.name == 'dir' && e.attributes['deleted']
927 e.delete_attribute('deleted')
930 restore_one.call($xmldir)
931 populate_subalbums_treeview(false)
932 $albums_tv.selection.select_path(sel[0])
935 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
938 frame1 = Gtk::Frame.new
939 fullpath = from_utf8("#{$current_path}/#{filename}")
941 my_gen_real_thumbnail = proc {
942 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
946 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
947 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
948 pack_start(img = Gtk::Image.new).
949 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
950 px, mask = pxb.render_pixmap_and_mask
951 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
952 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
954 frame1.add(img = Gtk::Image.new)
957 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
958 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
959 my_gen_real_thumbnail.call
961 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
964 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
966 tooltips = Gtk::Tooltips.new
967 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
968 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
970 frame2, textview = create_editzone($autotable_sw, 1, img)
971 textview.buffer.text = caption
972 textview.set_justification(Gtk::Justification::CENTER)
974 vbox = Gtk::VBox.new(false, 5)
975 vbox.pack_start(evtbox, false, false)
976 vbox.pack_start(frame2, false, false)
977 autotable.append(vbox, filename)
979 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
980 $vbox2widgets[vbox] = { :textview => textview, :image => img }
982 #- to be able to find widgets by name
983 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
985 cleanup_all_thumbnails = proc {
986 #- remove out of sync images
987 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
988 for sizeobj in $images_size
989 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
995 cleanup_all_thumbnails.call
996 my_gen_real_thumbnail.call
999 rotate_and_cleanup = proc { |angle|
1000 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1001 cleanup_all_thumbnails.call
1004 move = proc { |direction|
1005 do_method = "move_#{direction}"
1006 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1008 done = autotable.method(do_method).call(vbox)
1009 textview.grab_focus #- because if moving, focus is stolen
1013 save_undo(_("move %s") % direction,
1015 autotable.method(undo_method).call(vbox)
1016 textview.grab_focus #- because if moving, focus is stolen
1017 autoscroll_if_needed($autotable_sw, img, textview)
1018 $notebook.set_page(1)
1020 autotable.method(do_method).call(vbox)
1021 textview.grab_focus #- because if moving, focus is stolen
1022 autoscroll_if_needed($autotable_sw, img, textview)
1023 $notebook.set_page(1)
1029 color_swap_and_cleanup = proc {
1030 perform_color_swap_and_cleanup = proc {
1031 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1032 my_gen_real_thumbnail.call
1035 cleanup_all_thumbnails.call
1036 perform_color_swap_and_cleanup.call
1038 save_undo(_("color swap"),
1040 perform_color_swap_and_cleanup.call
1042 autoscroll_if_needed($autotable_sw, img, textview)
1043 $notebook.set_page(1)
1045 perform_color_swap_and_cleanup.call
1047 autoscroll_if_needed($autotable_sw, img, textview)
1048 $notebook.set_page(1)
1053 change_frame_offset_and_cleanup_real = proc { |values|
1054 perform_change_frame_offset_and_cleanup = proc { |val|
1055 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
1056 my_gen_real_thumbnail.call
1058 perform_change_frame_offset_and_cleanup.call(values[:new])
1060 save_undo(_("specify frame offset"),
1062 perform_change_frame_offset_and_cleanup.call(values[:old])
1064 autoscroll_if_needed($autotable_sw, img, textview)
1065 $notebook.set_page(1)
1067 perform_change_frame_offset_and_cleanup.call(values[:new])
1069 autoscroll_if_needed($autotable_sw, img, textview)
1070 $notebook.set_page(1)
1075 change_frame_offset_and_cleanup = proc {
1076 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
1077 change_frame_offset_and_cleanup_real.call(values)
1081 change_pano_amount_and_cleanup_real = proc { |values|
1082 perform_change_pano_amount_and_cleanup = proc { |val|
1083 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1085 perform_change_pano_amount_and_cleanup.call(values[:new])
1087 save_undo(_("change panorama amount"),
1089 perform_change_pano_amount_and_cleanup.call(values[:old])
1091 autoscroll_if_needed($autotable_sw, img, textview)
1092 $notebook.set_page(1)
1094 perform_change_pano_amount_and_cleanup.call(values[:new])
1096 autoscroll_if_needed($autotable_sw, img, textview)
1097 $notebook.set_page(1)
1102 change_pano_amount_and_cleanup = proc {
1103 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1104 change_pano_amount_and_cleanup_real.call(values)
1108 whitebalance_and_cleanup_real = proc { |values|
1109 perform_change_whitebalance_and_cleanup = proc { |val|
1110 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1111 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1112 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1113 cleanup_all_thumbnails.call
1115 perform_change_whitebalance_and_cleanup.call(values[:new])
1117 save_undo(_("fix white balance"),
1119 perform_change_whitebalance_and_cleanup.call(values[:old])
1121 autoscroll_if_needed($autotable_sw, img, textview)
1122 $notebook.set_page(1)
1124 perform_change_whitebalance_and_cleanup.call(values[:new])
1126 autoscroll_if_needed($autotable_sw, img, textview)
1127 $notebook.set_page(1)
1132 whitebalance_and_cleanup = proc {
1133 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1134 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1135 whitebalance_and_cleanup_real.call(values)
1139 enhance_and_cleanup = proc {
1140 perform_enhance_and_cleanup = proc {
1141 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1142 my_gen_real_thumbnail.call
1145 cleanup_all_thumbnails.call
1146 perform_enhance_and_cleanup.call
1148 save_undo(_("enhance"),
1150 perform_enhance_and_cleanup.call
1152 autoscroll_if_needed($autotable_sw, img, textview)
1153 $notebook.set_page(1)
1155 perform_enhance_and_cleanup.call
1157 autoscroll_if_needed($autotable_sw, img, textview)
1158 $notebook.set_page(1)
1163 delete = proc { |isacut|
1164 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 })
1167 perform_delete = proc {
1168 after = autotable.get_next_widget(vbox)
1170 after = autotable.get_previous_widget(vbox)
1172 if $config['deleteondisk'] && !isacut
1173 msg 3, "scheduling for delete: #{fullpath}"
1174 $todelete << fullpath
1176 autotable.remove(vbox)
1178 $vbox2widgets[after][:textview].grab_focus
1179 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1183 previous_pos = autotable.get_current_number(vbox)
1187 delete_current_subalbum
1189 save_undo(_("delete"),
1191 autotable.reinsert(pos, vbox, filename)
1192 $notebook.set_page(1)
1193 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1195 msg 3, "removing deletion schedule of: #{fullpath}"
1196 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1199 $notebook.set_page(1)
1208 $cuts << { :vbox => vbox, :filename => filename }
1209 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1214 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1217 autotable.queue_draws << proc {
1218 $vbox2widgets[last[:vbox]][:textview].grab_focus
1219 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1221 save_undo(_("paste"),
1223 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1224 $notebook.set_page(1)
1227 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1229 $notebook.set_page(1)
1232 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1237 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1238 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1239 :whitebalance => whitebalance_and_cleanup_real, :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1241 textview.signal_connect('key-press-event') { |w, event|
1244 x, y = autotable.get_current_pos(vbox)
1245 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1246 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1247 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1248 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1250 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1251 $vbox2widgets[widget_up][:textview].grab_focus
1258 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1260 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1261 $vbox2widgets[widget_down][:textview].grab_focus
1268 if event.keyval == Gdk::Keyval::GDK_Left
1271 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1278 rotate_and_cleanup.call(-90)
1281 if event.keyval == Gdk::Keyval::GDK_Right
1282 next_ = autotable.get_next_widget(vbox)
1283 if next_ && autotable.get_current_pos(next_)[0] > x
1285 $vbox2widgets[next_][:textview].grab_focus
1292 rotate_and_cleanup.call(90)
1295 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1298 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1299 view_element(filename, { :delete => delete })
1302 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1305 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1309 !propagate #- propagate if needed
1312 $ignore_next_release = false
1313 evtbox.signal_connect('button-press-event') { |w, event|
1314 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1315 if event.state & Gdk::Window::BUTTON3_MASK != 0
1316 #- gesture redo: hold right mouse button then click left mouse button
1317 $config['nogestures'] or perform_redo
1318 $ignore_next_release = true
1320 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1322 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1324 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1325 elsif $enhance.active?
1326 enhance_and_cleanup.call
1327 elsif $delete.active?
1331 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1334 $button1_pressed_autotable = true
1335 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1336 if event.state & Gdk::Window::BUTTON1_MASK != 0
1337 #- gesture undo: hold left mouse button then click right mouse button
1338 $config['nogestures'] or perform_undo
1339 $ignore_next_release = true
1341 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1342 view_element(filename, { :delete => delete })
1347 evtbox.signal_connect('button-release-event') { |w, event|
1348 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1349 if !$ignore_next_release
1350 x, y = autotable.get_current_pos(vbox)
1351 next_ = autotable.get_next_widget(vbox)
1352 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1353 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1354 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1355 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1356 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1357 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1358 :pano => change_pano_amount_and_cleanup, :refresh => refresh })
1360 $ignore_next_release = false
1361 $gesture_press = nil
1366 #- handle reordering with drag and drop
1367 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1368 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1369 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1370 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1373 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1375 #- mouse gesture first (dnd disables button-release-event)
1376 if $gesture_press && $gesture_press[:filename] == filename
1377 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1378 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1379 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1380 rotate_and_cleanup.call(angle)
1381 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1383 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1384 msg 3, "gesture delete: click-drag right button to the bottom"
1386 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1391 ctxt.targets.each { |target|
1392 if target.name == 'reorder-elements'
1393 move_dnd = proc { |from,to|
1396 autotable.move(from, to)
1397 save_undo(_("reorder"),
1400 autotable.move(to - 1, from)
1402 autotable.move(to, from + 1)
1404 $notebook.set_page(1)
1406 autotable.move(from, to)
1407 $notebook.set_page(1)
1412 if $multiple_dnd.size == 0
1413 move_dnd.call(selection_data.data.to_i,
1414 autotable.get_current_number(vbox))
1416 UndoHandler.begin_batch
1417 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1419 #- need to update current position between each call
1420 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1421 autotable.get_current_number(vbox))
1423 UndoHandler.end_batch
1434 def create_auto_table
1436 $autotable = Gtk::AutoTable.new(5)
1438 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1439 thumbnails_vb = Gtk::VBox.new(false, 5)
1441 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1442 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1443 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1444 thumbnails_vb.add($autotable)
1446 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1447 $autotable_sw.add_with_viewport(thumbnails_vb)
1449 #- follows stuff for handling multiple elements selection
1450 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1452 update_selected = proc {
1453 $autotable.current_order.each { |path|
1454 w = $name2widgets[path][:evtbox].window
1455 xm = w.position[0] + w.size[0]/2
1456 ym = w.position[1] + w.size[1]/2
1457 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1458 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1459 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1460 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1463 if $selected_elements[path] && ! $selected_elements[path][:keep]
1464 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))
1465 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1466 $selected_elements.delete(path)
1471 $autotable.signal_connect('realize') { |w,e|
1472 gc = Gdk::GC.new($autotable.window)
1473 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1474 gc.function = Gdk::GC::INVERT
1475 #- autoscroll handling for DND and multiple selections
1476 Gtk.timeout_add(100) {
1477 if ! $autotable.window.nil?
1478 w, x, y, mask = $autotable.window.pointer
1479 if mask & Gdk::Window::BUTTON1_MASK != 0
1480 if y < $autotable_sw.vadjustment.value
1482 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1484 if $button1_pressed_autotable || press_x
1485 scroll_upper($autotable_sw, y)
1488 w, pos_x, pos_y = $autotable.window.pointer
1489 $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]])
1490 update_selected.call
1493 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1495 $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]])
1497 if $button1_pressed_autotable || press_x
1498 scroll_lower($autotable_sw, y)
1501 w, pos_x, pos_y = $autotable.window.pointer
1502 $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]])
1503 update_selected.call
1508 ! $autotable.window.nil?
1512 $autotable.signal_connect('button-press-event') { |w,e|
1514 if !$button1_pressed_autotable
1517 if e.state & Gdk::Window::SHIFT_MASK == 0
1518 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1519 $selected_elements = {}
1520 $statusbar.push(0, utf8(_("Nothing selected.")))
1522 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1524 set_mousecursor(Gdk::Cursor::TCROSS)
1528 $autotable.signal_connect('button-release-event') { |w,e|
1530 if $button1_pressed_autotable
1531 #- unselect all only now
1532 $multiple_dnd = $selected_elements.keys
1533 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1534 $selected_elements = {}
1535 $button1_pressed_autotable = false
1538 $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]])
1539 if $selected_elements.length > 0
1540 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1543 press_x = press_y = pos_x = pos_y = nil
1544 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1548 $autotable.signal_connect('motion-notify-event') { |w,e|
1551 $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]])
1555 $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]])
1556 update_selected.call
1562 def create_subalbums_page
1564 subalbums_hb = Gtk::HBox.new
1565 $subalbums_vb = Gtk::VBox.new(false, 5)
1566 subalbums_hb.pack_start($subalbums_vb, false, false)
1567 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1568 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1569 $subalbums_sw.add_with_viewport(subalbums_hb)
1572 def save_current_file
1578 ios = File.open($filename, "w")
1579 $xmldoc.write(ios, 0)
1581 rescue Iconv::IllegalSequence
1582 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1583 if ! ios.nil? && ! ios.closed?
1586 $xmldoc.xml_decl.encoding = 'UTF-8'
1587 ios = File.open($filename, "w")
1588 $xmldoc.write(ios, 0)
1598 def save_current_file_user
1599 save_tempfilename = $filename
1600 $filename = $orig_filename
1601 if ! save_current_file
1602 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1603 $filename = save_tempfilename
1607 $generated_outofline = false
1608 $filename = save_tempfilename
1610 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1611 $todelete.each { |f|
1612 system("rm -f #{f}")
1616 def mark_document_as_dirty
1617 $xmldoc.elements.each('//dir') { |elem|
1618 elem.delete_attribute('already-generated')
1622 #- ret: true => ok false => cancel
1623 def ask_save_modifications(msg1, msg2, *options)
1625 options = options.size > 0 ? options[0] : {}
1627 if options[:disallow_cancel]
1628 dialog = Gtk::Dialog.new(msg1,
1630 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1631 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1632 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1634 dialog = Gtk::Dialog.new(msg1,
1636 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1637 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1638 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1639 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1641 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1642 dialog.vbox.add(Gtk::Label.new(msg2))
1643 dialog.window_position = Gtk::Window::POS_CENTER
1646 dialog.run { |response|
1648 if response == Gtk::Dialog::RESPONSE_YES
1649 if ! save_current_file_user
1650 return ask_save_modifications(msg1, msg2, options)
1653 #- if we have generated an album but won't save modifications, we must remove
1654 #- already-generated markers in original file
1655 if $generated_outofline
1657 $xmldoc = REXML::Document.new File.new($orig_filename)
1658 mark_document_as_dirty
1659 ios = File.open($orig_filename, "w")
1660 $xmldoc.write(ios, 0)
1663 puts "exception: #{$!}"
1667 if response == Gtk::Dialog::RESPONSE_CANCEL
1670 $todelete = [] #- unconditionally clear the list of images/videos to delete
1676 def try_quit(*options)
1677 if ask_save_modifications(utf8(_("Save before quitting?")),
1678 utf8(_("Do you want to save your changes before quitting?")),
1684 def show_popup(parent, msg, *options)
1685 dialog = Gtk::Dialog.new
1686 if options[0] && options[0][:title]
1687 dialog.title = options[0][:title]
1689 dialog.title = utf8(_("Booh message"))
1691 lbl = Gtk::Label.new
1692 if options[0] && options[0][:nomarkup]
1697 if options[0] && options[0][:centered]
1698 lbl.set_justify(Gtk::Justification::CENTER)
1700 if options[0] && options[0][:selectable]
1701 lbl.selectable = true
1703 if options[0] && options[0][:topwidget]
1704 dialog.vbox.add(options[0][:topwidget])
1706 if options[0] && options[0][:scrolled]
1707 sw = Gtk::ScrolledWindow.new(nil, nil)
1708 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1709 sw.add_with_viewport(lbl)
1711 dialog.set_default_size(500, 600)
1713 dialog.vbox.add(lbl)
1714 dialog.set_default_size(200, 120)
1716 if options[0] && options[0][:okcancel]
1717 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1719 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1721 if options[0] && options[0][:pos_centered]
1722 dialog.window_position = Gtk::Window::POS_CENTER
1724 dialog.window_position = Gtk::Window::POS_MOUSE
1727 if options[0] && options[0][:linkurl]
1728 linkbut = Gtk::Button.new('')
1729 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1730 linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1731 linkbut.relief = Gtk::RELIEF_NONE
1732 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1733 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1734 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1739 if !options[0] || !options[0][:not_transient]
1740 dialog.transient_for = parent
1741 dialog.run { |response|
1743 if options[0] && options[0][:okcancel]
1744 return response == Gtk::Dialog::RESPONSE_OK
1748 dialog.signal_connect('response') { dialog.destroy }
1752 def backend_wait_message(parent, msg, infopipe_path, mode)
1754 w.set_transient_for(parent)
1757 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1758 vb.pack_start(Gtk::Label.new(msg), false, false)
1760 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1761 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1762 if mode != 'one dir scan'
1763 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1765 if mode == 'web-album'
1766 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1767 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1769 vb.pack_start(Gtk::HSeparator.new, false, false)
1771 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1772 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1773 vb.pack_end(bottom, false, false)
1775 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1776 refresh_thread = Thread.new {
1777 directories_counter = 0
1778 while line = infopipe.gets
1779 if line =~ /^directories: (\d+), sizes: (\d+)/
1780 directories = $1.to_f + 1
1782 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1783 elements = $3.to_f + 1
1784 if mode == 'web-album'
1788 gtk_thread_protect { pb1_1.fraction = 0 }
1789 if mode != 'one dir scan'
1790 newtext = utf8(full_src_dir_to_rel($1, $2))
1791 newtext = '/' if newtext == ''
1792 gtk_thread_protect { pb1_2.text = newtext }
1793 directories_counter += 1
1794 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1796 elsif line =~ /^processing element$/
1797 element_counter += 1
1798 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1799 elsif line =~ /^processing size$/
1800 element_counter += 1
1801 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1802 elsif line =~ /^finished processing sizes$/
1803 gtk_thread_protect { pb1_1.fraction = 1 }
1804 elsif line =~ /^creating index.html$/
1805 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1806 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1807 directories_counter = 0
1808 elsif line =~ /^index.html: (.+)\|(.+)/
1809 newtext = utf8(full_src_dir_to_rel($1, $2))
1810 newtext = '/' if newtext == ''
1811 gtk_thread_protect { pb2.text = newtext }
1812 directories_counter += 1
1813 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1814 elsif line =~ /^die: (.*)$/
1821 w.signal_connect('delete-event') { w.destroy }
1822 w.signal_connect('destroy') {
1823 Thread.kill(refresh_thread)
1824 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1827 system("rm -f #{infopipe_path}")
1830 w.window_position = Gtk::Window::POS_CENTER
1836 def call_backend(cmd, waitmsg, mode, params)
1837 pipe = Tempfile.new("boohpipe")
1839 system("mkfifo #{pipe.path}")
1840 cmd += " --info-pipe #{pipe.path}"
1841 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
1846 id, exitstatus = Process.waitpid2(pid)
1847 gtk_thread_protect { w8.destroy }
1849 if params[:successmsg]
1850 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
1852 if params[:closure_after]
1853 gtk_thread_protect(¶ms[:closure_after])
1855 elsif exitstatus == 15
1856 #- say nothing, user aborted
1858 gtk_thread_protect { show_popup($main_window,
1859 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
1865 button.signal_connect('clicked') {
1866 Process.kill('SIGTERM', pid)
1870 def save_changes(*forced)
1871 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1875 $xmldir.delete_attribute('already-generated')
1877 propagate_children = proc { |xmldir|
1878 if xmldir.attributes['subdirs-caption']
1879 xmldir.delete_attribute('already-generated')
1881 xmldir.elements.each('dir') { |element|
1882 propagate_children.call(element)
1886 if $xmldir.child_byname_notattr('dir', 'deleted')
1887 new_title = $subalbums_title.buffer.text
1888 if new_title != $xmldir.attributes['subdirs-caption']
1889 parent = $xmldir.parent
1890 if parent.name == 'dir'
1891 parent.delete_attribute('already-generated')
1893 propagate_children.call($xmldir)
1895 $xmldir.add_attribute('subdirs-caption', new_title)
1896 $xmldir.elements.each('dir') { |element|
1897 if !element.attributes['deleted']
1898 path = element.attributes['path']
1899 newtext = $subalbums_edits[path][:editzone].buffer.text
1900 if element.attributes['subdirs-caption']
1901 if element.attributes['subdirs-caption'] != newtext
1902 propagate_children.call(element)
1904 element.add_attribute('subdirs-caption', newtext)
1905 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1907 if element.attributes['thumbnails-caption'] != newtext
1908 element.delete_attribute('already-generated')
1910 element.add_attribute('thumbnails-caption', newtext)
1911 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1917 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
1918 if $xmldir.attributes['thumbnails-caption']
1919 path = $xmldir.attributes['path']
1920 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1922 elsif $xmldir.attributes['thumbnails-caption']
1923 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
1926 #- remove and reinsert elements to reflect new ordering
1929 $xmldir.elements.each { |element|
1930 if element.name == 'image' || element.name == 'video'
1931 saves[element.attributes['filename']] = element.remove
1935 $autotable.current_order.each { |path|
1936 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1937 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1940 saves.each_key { |path|
1941 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1942 chld.add_attribute('deleted', 'true')
1946 def sort_by_exif_date
1950 $xmldir.elements.each { |element|
1951 if element.name == 'image' || element.name == 'video'
1952 current_order << element.attributes['filename']
1956 #- look for EXIF dates
1958 w.set_transient_for($main_window)
1960 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1961 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
1962 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
1963 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1964 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1965 vb.pack_end(bottom, false, false)
1967 w.signal_connect('delete-event') { w.destroy }
1968 w.window_position = Gtk::Window::POS_CENTER
1972 b.signal_connect('clicked') { aborted = true }
1975 current_order.each { |f|
1977 if entry2type(f) == 'image'
1979 pb.fraction = i.to_f / current_order.size
1980 Gtk.main_iteration while Gtk.events_pending?
1981 date_time = `identify -format "%[EXIF:DateTime]" '#{from_utf8($current_path + "/" + f)}'`.chomp
1982 if $? == 0 && date_time != ''
1983 dates[f] = date_time
1996 $xmldir.elements.each { |element|
1997 if element.name == 'image' || element.name == 'video'
1998 saves[element.attributes['filename']] = element.remove
2002 #- find a good fallback for all entries without a date (still next to the item they were next to)
2003 neworder = dates.keys.sort { |a,b| dates[a] <=> dates[b] }
2004 for i in 0 .. current_order.size - 1
2005 if ! neworder.include?(current_order[i])
2007 while j > 0 && ! neworder.include?(current_order[j])
2010 neworder[(neworder.index(current_order[j]) || -1 ) + 1, 0] = current_order[i]
2014 $xmldir.add_element(saves[f].name, saves[f].attributes)
2017 #- let the auto-table reflect new ordering
2021 def remove_all_captions
2024 $autotable.current_order.each { |path|
2025 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2026 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2028 save_undo(_("remove all captions"),
2030 texts.each_key { |key|
2031 $name2widgets[key][:textview].buffer.text = texts[key]
2033 $notebook.set_page(1)
2035 texts.each_key { |key|
2036 $name2widgets[key][:textview].buffer.text = ''
2038 $notebook.set_page(1)
2044 $selected_elements.each_key { |path|
2045 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2051 $selected_elements = {}
2055 $undo_tb.sensitive = $undo_mb.sensitive = false
2056 $redo_tb.sensitive = $redo_mb.sensitive = false
2062 $subalbums_vb.children.each { |chld|
2063 $subalbums_vb.remove(chld)
2065 $subalbums = Gtk::Table.new(0, 0, true)
2066 current_y_sub_albums = 0
2068 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2069 $subalbums_edits = {}
2070 subalbums_counter = 0
2071 subalbums_edits_bypos = {}
2073 add_subalbum = proc { |xmldir, counter|
2074 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2075 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2076 if xmldir == $xmldir
2077 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2078 caption = xmldir.attributes['thumbnails-caption']
2079 captionfile, dummy = find_subalbum_caption_info(xmldir)
2080 infotype = 'thumbnails'
2082 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2083 captionfile, caption = find_subalbum_caption_info(xmldir)
2084 infotype = find_subalbum_info_type(xmldir)
2086 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2087 hbox = Gtk::HBox.new
2088 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2090 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2093 my_gen_real_thumbnail = proc {
2094 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2097 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2098 f.add(img = Gtk::Image.new)
2099 my_gen_real_thumbnail.call
2101 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2103 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2104 $subalbums.attach(hbox,
2105 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2107 frame, textview = create_editzone($subalbums_sw, 0, img)
2108 textview.buffer.text = caption
2109 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2110 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2112 change_image = proc {
2113 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2115 Gtk::FileChooser::ACTION_OPEN,
2117 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2118 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2119 fc.transient_for = $main_window
2120 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))
2121 f.add(preview_img = Gtk::Image.new)
2123 fc.signal_connect('update-preview') { |w|
2125 if fc.preview_filename
2126 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2127 fc.preview_widget_active = true
2129 rescue Gdk::PixbufError
2130 fc.preview_widget_active = false
2133 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2135 old_file = captionfile
2136 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2137 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2138 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2139 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2141 new_file = fc.filename
2142 msg 3, "new captionfile is: #{fc.filename}"
2143 perform_changefile = proc {
2144 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2145 $modified_pixbufs.delete(thumbnail_file)
2146 xmldir.delete_attribute("#{infotype}-rotate")
2147 xmldir.delete_attribute("#{infotype}-color-swap")
2148 xmldir.delete_attribute("#{infotype}-enhance")
2149 xmldir.delete_attribute("#{infotype}-frame-offset")
2150 my_gen_real_thumbnail.call
2152 perform_changefile.call
2154 save_undo(_("change caption file for sub-album"),
2156 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2157 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2158 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2159 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2160 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2161 my_gen_real_thumbnail.call
2162 $notebook.set_page(0)
2164 perform_changefile.call
2165 $notebook.set_page(0)
2173 system("rm -f '#{thumbnail_file}'")
2174 my_gen_real_thumbnail.call
2177 rotate_and_cleanup = proc { |angle|
2178 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2179 system("rm -f '#{thumbnail_file}'")
2182 move = proc { |direction|
2185 save_changes('forced')
2186 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2187 if direction == 'up'
2188 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2189 subalbums_edits_bypos[oldpos - 1][:position] += 1
2191 if direction == 'down'
2192 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2193 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2195 if direction == 'top'
2196 for i in 1 .. oldpos - 1
2197 subalbums_edits_bypos[i][:position] += 1
2199 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2201 if direction == 'bottom'
2202 for i in oldpos + 1 .. subalbums_counter
2203 subalbums_edits_bypos[i][:position] -= 1
2205 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2209 $xmldir.elements.each('dir') { |element|
2210 if (!element.attributes['deleted'])
2211 elems << [ element.attributes['path'], element.remove ]
2214 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2215 each { |e| $xmldir.add_element(e[1]) }
2216 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2217 $xmldir.elements.each('descendant::dir') { |elem|
2218 elem.delete_attribute('already-generated')
2221 sel = $albums_tv.selection.selected_rows
2223 populate_subalbums_treeview(false)
2224 $albums_tv.selection.select_path(sel[0])
2227 color_swap_and_cleanup = proc {
2228 perform_color_swap_and_cleanup = proc {
2229 color_swap(xmldir, "#{infotype}-")
2230 my_gen_real_thumbnail.call
2232 perform_color_swap_and_cleanup.call
2234 save_undo(_("color swap"),
2236 perform_color_swap_and_cleanup.call
2237 $notebook.set_page(0)
2239 perform_color_swap_and_cleanup.call
2240 $notebook.set_page(0)
2245 change_frame_offset_and_cleanup = proc {
2246 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2247 perform_change_frame_offset_and_cleanup = proc { |val|
2248 change_frame_offset(xmldir, "#{infotype}-", val)
2249 my_gen_real_thumbnail.call
2251 perform_change_frame_offset_and_cleanup.call(values[:new])
2253 save_undo(_("specify frame offset"),
2255 perform_change_frame_offset_and_cleanup.call(values[:old])
2256 $notebook.set_page(0)
2258 perform_change_frame_offset_and_cleanup.call(values[:new])
2259 $notebook.set_page(0)
2265 whitebalance_and_cleanup = proc {
2266 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2267 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2268 perform_change_whitebalance_and_cleanup = proc { |val|
2269 change_whitebalance(xmldir, "#{infotype}-", val)
2270 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2271 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2272 system("rm -f '#{thumbnail_file}'")
2274 perform_change_whitebalance_and_cleanup.call(values[:new])
2276 save_undo(_("fix white balance"),
2278 perform_change_whitebalance_and_cleanup.call(values[:old])
2279 $notebook.set_page(0)
2281 perform_change_whitebalance_and_cleanup.call(values[:new])
2282 $notebook.set_page(0)
2288 enhance_and_cleanup = proc {
2289 perform_enhance_and_cleanup = proc {
2290 enhance(xmldir, "#{infotype}-")
2291 my_gen_real_thumbnail.call
2294 perform_enhance_and_cleanup.call
2296 save_undo(_("enhance"),
2298 perform_enhance_and_cleanup.call
2299 $notebook.set_page(0)
2301 perform_enhance_and_cleanup.call
2302 $notebook.set_page(0)
2307 evtbox.signal_connect('button-press-event') { |w, event|
2308 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2310 rotate_and_cleanup.call(90)
2312 rotate_and_cleanup.call(-90)
2313 elsif $enhance.active?
2314 enhance_and_cleanup.call
2317 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2318 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2319 { :forbid_left => true, :forbid_right => true,
2320 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2321 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2322 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2323 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2324 :refresh => refresh })
2326 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2331 evtbox.signal_connect('button-press-event') { |w, event|
2332 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2336 evtbox.signal_connect('button-release-event') { |w, event|
2337 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2338 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2339 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2340 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2341 msg 3, "gesture rotate: #{angle}"
2342 rotate_and_cleanup.call(angle)
2345 $gesture_press = nil
2348 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2349 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2350 current_y_sub_albums += 1
2353 if $xmldir.child_byname_notattr('dir', 'deleted')
2355 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2356 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2357 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2358 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2359 #- this album image/caption
2360 if $xmldir.attributes['thumbnails-caption']
2361 add_subalbum.call($xmldir, 0)
2364 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2365 $xmldir.elements.each { |element|
2366 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2367 #- element (image or video) of this album
2368 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2369 msg 3, "dest_img: #{dest_img}"
2370 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2371 total[element.name] += 1
2373 if element.name == 'dir' && !element.attributes['deleted']
2374 #- sub-album image/caption
2375 add_subalbum.call(element, subalbums_counter += 1)
2376 total[element.name] += 1
2379 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2380 total['image'], total['video'], total['dir'] ]))
2381 $subalbums_vb.add($subalbums)
2382 $subalbums_vb.show_all
2384 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2385 $notebook.get_tab_label($autotable_sw).sensitive = false
2386 $notebook.set_page(0)
2387 $thumbnails_title.buffer.text = ''
2389 $notebook.get_tab_label($autotable_sw).sensitive = true
2390 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2393 if !$xmldir.child_byname_notattr('dir', 'deleted')
2394 $notebook.get_tab_label($subalbums_sw).sensitive = false
2395 $notebook.set_page(1)
2397 $notebook.get_tab_label($subalbums_sw).sensitive = true
2401 def pixbuf_or_nil(filename)
2403 return Gdk::Pixbuf.new(filename)
2409 def theme_choose(current)
2410 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2412 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2413 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2414 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2416 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2417 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2418 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2419 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2420 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2421 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2422 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2423 treeview.signal_connect('button-press-event') { |w, event|
2424 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2425 dialog.response(Gtk::Dialog::RESPONSE_OK)
2429 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2431 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2434 iter[0] = File.basename(dir)
2435 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2436 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2437 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2438 if File.basename(dir) == current
2439 treeview.selection.select_iter(iter)
2443 dialog.set_default_size(700, 400)
2444 dialog.vbox.show_all
2445 dialog.run { |response|
2446 iter = treeview.selection.selected
2448 if response == Gtk::Dialog::RESPONSE_OK && iter
2449 return model.get_value(iter, 0)
2455 def show_password_protections
2456 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2457 child_iter = $albums_iters[xmldir.attributes['path']]
2458 if xmldir.attributes['password-protect']
2459 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2460 already_protected = true
2461 elsif already_protected
2462 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2464 pix = pix.saturate_and_pixelate(1, true)
2470 xmldir.elements.each('dir') { |elem|
2471 if !elem.attributes['deleted']
2472 examine_dir_elem.call(child_iter, elem, already_protected)
2476 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2479 def populate_subalbums_treeview(select_first)
2483 $subalbums_vb.children.each { |chld|
2484 $subalbums_vb.remove(chld)
2487 source = $xmldoc.root.attributes['source']
2488 msg 3, "source: #{source}"
2490 xmldir = $xmldoc.elements['//dir']
2491 if !xmldir || xmldir.attributes['path'] != source
2492 msg 1, _("Corrupted booh file...")
2496 append_dir_elem = proc { |parent_iter, xmldir|
2497 child_iter = $albums_ts.append(parent_iter)
2498 child_iter[0] = File.basename(xmldir.attributes['path'])
2499 child_iter[1] = xmldir.attributes['path']
2500 $albums_iters[xmldir.attributes['path']] = child_iter
2501 msg 3, "puttin location: #{xmldir.attributes['path']}"
2502 xmldir.elements.each('dir') { |elem|
2503 if !elem.attributes['deleted']
2504 append_dir_elem.call(child_iter, elem)
2508 append_dir_elem.call(nil, xmldir)
2509 show_password_protections
2511 $albums_tv.expand_all
2513 $albums_tv.selection.select_iter($albums_ts.iter_first)
2517 def open_file(filename)
2521 $current_path = nil #- invalidate
2522 $modified_pixbufs = {}
2525 $subalbums_vb.children.each { |chld|
2526 $subalbums_vb.remove(chld)
2529 if !File.exists?(filename)
2530 return utf8(_("File not found."))
2534 $xmldoc = REXML::Document.new File.new(filename)
2539 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2540 if entry2type(filename).nil?
2541 return utf8(_("Not a booh file!"))
2543 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."))
2547 if !source = $xmldoc.root.attributes['source']
2548 return utf8(_("Corrupted booh file..."))
2551 if !dest = $xmldoc.root.attributes['destination']
2552 return utf8(_("Corrupted booh file..."))
2555 if !theme = $xmldoc.root.attributes['theme']
2556 return utf8(_("Corrupted booh file..."))
2559 if $xmldoc.root.attributes['version'] < '0.8.4'
2560 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2561 mark_document_as_dirty
2562 if $xmldoc.root.attributes['version'] < '0.8.4'
2563 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2564 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2565 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2566 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2567 if old_dest_dir != new_dest_dir
2568 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2570 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2571 xmldir.elements.each { |element|
2572 if %w(image video).include?(element.name) && !element.attributes['deleted']
2573 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2574 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2575 Dir[old_name + '*'].each { |file|
2576 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2577 file != new_file and sys("mv '#{file}' '#{new_file}'")
2580 if element.name == 'dir' && !element.attributes['deleted']
2581 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2582 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2583 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2587 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2591 $xmldoc.root.add_attribute('version', $VERSION)
2594 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2595 optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2596 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2598 $filename = filename
2599 select_theme(theme, limit_sizes, optimizefor32, nperrow)
2600 $default_size['thumbnails'] =~ /(.*)x(.*)/
2601 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2602 $albums_thumbnail_size =~ /(.*)x(.*)/
2603 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2605 populate_subalbums_treeview(true)
2607 $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
2611 def open_file_user(filename)
2612 result = open_file(filename)
2614 $config['last-opens'] ||= []
2615 if $config['last-opens'][-1] != utf8(filename)
2616 $config['last-opens'] << utf8(filename)
2618 $orig_filename = $filename
2619 tmp = Tempfile.new("boohtemp")
2622 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2624 $tempfiles << $filename << "#{$filename}.backup"
2626 $orig_filename = nil
2632 if !ask_save_modifications(utf8(_("Save this album?")),
2633 utf8(_("Do you want to save the changes to this album?")),
2634 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2637 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2639 Gtk::FileChooser::ACTION_OPEN,
2641 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2642 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2643 fc.set_current_folder(File.expand_path("~/.booh"))
2644 fc.transient_for = $main_window
2647 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2648 push_mousecursor_wait(fc)
2649 msg = open_file_user(fc.filename)
2665 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2670 def additional_booh_options
2673 options += "--mproc #{$config['mproc'].to_i} "
2675 options += "--comments-format '#{$config['comments-format']}'"
2680 if !ask_save_modifications(utf8(_("Save this album?")),
2681 utf8(_("Do you want to save the changes to this album?")),
2682 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2685 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2687 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2688 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2689 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2691 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2692 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2693 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2694 tbl.attach(src = Gtk::Entry.new,
2695 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2696 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2697 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2698 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2699 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2700 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2701 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2702 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2703 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2704 tbl.attach(dest = Gtk::Entry.new,
2705 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2706 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2707 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2708 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2709 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2710 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2711 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2712 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2713 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2715 tooltips = Gtk::Tooltips.new
2716 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2717 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2718 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2719 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2720 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2721 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2722 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)
2723 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2724 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2725 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2726 pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
2727 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)
2729 src_nb_calculated_for = ''
2731 process_src_nb = proc {
2732 if src.text != src_nb_calculated_for
2733 src_nb_calculated_for = src.text
2735 Thread.kill(src_nb_thread)
2738 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
2739 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
2741 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2742 if File.readable?(from_utf8_safe(src_nb_calculated_for))
2743 src_nb_thread = Thread.new {
2744 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2745 total = { 'image' => 0, 'video' => 0, nil => 0 }
2746 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2747 if File.basename(dir) =~ /^\./
2751 Dir.entries(dir.chomp).each { |file|
2752 total[entry2type(file)] += 1
2754 rescue Errno::EACCES, Errno::ENOENT
2758 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2762 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2765 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2771 timeout_src_nb = Gtk.timeout_add(100) {
2775 src_browse.signal_connect('clicked') {
2776 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2778 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2780 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2781 fc.transient_for = $main_window
2782 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2783 src.text = utf8(fc.filename)
2785 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2790 dest_browse.signal_connect('clicked') {
2791 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2793 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2795 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2796 fc.transient_for = $main_window
2797 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2798 dest.text = utf8(fc.filename)
2803 conf_browse.signal_connect('clicked') {
2804 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2806 Gtk::FileChooser::ACTION_SAVE,
2808 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2809 fc.transient_for = $main_window
2810 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2811 fc.set_current_folder(File.expand_path("~/.booh"))
2812 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2813 conf.text = utf8(fc.filename)
2820 recreate_theme_config = proc {
2821 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2823 select_theme(theme_button.label, 'all', optimize432.active?, nil)
2824 $images_size.each { |s|
2825 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2829 tooltips.set_tip(cb, utf8(s['description']), nil)
2830 theme_sizes << { :widget => cb, :value => s['name'] }
2832 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2833 tooltips = Gtk::Tooltips.new
2834 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2835 theme_sizes << { :widget => cb, :value => 'original' }
2838 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2841 $allowed_N_values.each { |n|
2843 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2845 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2847 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2851 nperrows << { :widget => rb, :value => n }
2853 nperrowradios.show_all
2855 recreate_theme_config.call
2857 theme_button.signal_connect('clicked') {
2858 if newtheme = theme_choose(theme_button.label)
2859 theme_button.label = newtheme
2860 recreate_theme_config.call
2864 dialog.vbox.add(frame1)
2865 dialog.vbox.add(frame2)
2866 dialog.window_position = Gtk::Window::POS_MOUSE
2872 dialog.run { |response|
2873 if response == Gtk::Dialog::RESPONSE_OK
2874 srcdir = from_utf8_safe(src.text)
2875 destdir = from_utf8_safe(dest.text)
2876 confpath = from_utf8_safe(conf.text)
2877 if src.text != '' && srcdir == ''
2878 show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
2880 elsif !File.directory?(srcdir)
2881 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2883 elsif dest.text != '' && destdir == ''
2884 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
2886 elsif destdir != make_dest_filename(destdir)
2887 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2889 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
2890 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2892 elsif File.exists?(destdir) && !File.directory?(destdir)
2893 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2895 elsif conf.text == ''
2896 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2898 elsif conf.text != '' && confpath == ''
2899 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
2901 elsif File.directory?(confpath)
2902 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
2904 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2905 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2907 system("mkdir '#{destdir}'")
2908 if !File.directory?(destdir)
2909 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2921 srcdir = from_utf8(src.text)
2922 destdir = from_utf8(dest.text)
2923 configskel = File.expand_path(from_utf8(conf.text))
2924 theme = theme_button.label
2925 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2926 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2927 opt432 = optimize432.active?
2928 madewith = madewithentry.text
2931 Thread.kill(src_nb_thread)
2932 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2935 Gtk.timeout_remove(timeout_src_nb)
2938 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2939 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2940 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' #{additional_booh_options}",
2941 utf8(_("Please wait while scanning source directory...")),
2943 { :closure_after => proc { open_file_user(configskel) } })
2948 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2950 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2951 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2952 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2954 source = $xmldoc.root.attributes['source']
2955 dest = $xmldoc.root.attributes['destination']
2956 theme = $xmldoc.root.attributes['theme']
2957 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2958 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2959 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2961 limit_sizes = limit_sizes.split(/,/)
2963 madewith = $xmldoc.root.attributes['made-with']
2965 tooltips = Gtk::Tooltips.new
2966 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2967 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2968 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2969 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2970 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2971 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2972 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2973 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
2974 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2975 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2976 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2977 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
2978 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2980 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2981 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2982 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2983 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2984 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2985 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2986 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)
2987 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2988 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2989 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2990 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2992 madewithentry.text = madewith
2994 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)
2998 recreate_theme_config = proc {
2999 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3001 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3003 $images_size.each { |s|
3004 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3006 if limit_sizes.include?(s['name'])
3014 tooltips.set_tip(cb, utf8(s['description']), nil)
3015 theme_sizes << { :widget => cb, :value => s['name'] }
3017 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3018 tooltips = Gtk::Tooltips.new
3019 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3020 if limit_sizes && limit_sizes.include?('original')
3023 theme_sizes << { :widget => cb, :value => 'original' }
3026 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3029 $allowed_N_values.each { |n|
3031 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3033 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3035 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3036 nperrowradios.add(Gtk::Label.new(' '))
3037 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3040 nperrows << { :widget => rb, :value => n.to_s }
3042 nperrowradios.show_all
3044 recreate_theme_config.call
3046 theme_button.signal_connect('clicked') {
3047 if newtheme = theme_choose(theme_button.label)
3050 theme_button.label = newtheme
3051 recreate_theme_config.call
3055 dialog.vbox.add(frame1)
3056 dialog.vbox.add(frame2)
3057 dialog.window_position = Gtk::Window::POS_MOUSE
3063 dialog.run { |response|
3064 if response == Gtk::Dialog::RESPONSE_OK
3065 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3066 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3075 save_theme = theme_button.label
3076 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3077 save_opt432 = optimize432.active?
3078 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3079 save_madewith = madewithentry.text
3082 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith)
3083 mark_document_as_dirty
3085 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3086 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3087 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' #{additional_booh_options}",
3088 utf8(_("Please wait while scanning source directory...")),
3090 { :closure_after => proc {
3091 open_file($filename)
3100 sel = $albums_tv.selection.selected_rows
3102 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3103 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3104 utf8(_("Please wait while scanning source directory...")),
3106 { :closure_after => proc {
3107 open_file($filename)
3108 $albums_tv.selection.select_path(sel[0])
3116 sel = $albums_tv.selection.selected_rows
3118 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3119 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3120 utf8(_("Please wait while scanning source directory...")),
3122 { :closure_after => proc {
3123 open_file($filename)
3124 $albums_tv.selection.select_path(sel[0])
3132 theme = $xmldoc.root.attributes['theme']
3133 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3135 limit_sizes = "--sizes #{limit_sizes}"
3137 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3138 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3139 utf8(_("Please wait while scanning source directory...")),
3141 { :closure_after => proc {
3142 open_file($filename)
3148 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3150 Gtk::FileChooser::ACTION_SAVE,
3152 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3153 fc.transient_for = $main_window
3154 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3155 fc.set_current_folder(File.expand_path("~/.booh"))
3156 fc.filename = $orig_filename
3157 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3158 $orig_filename = fc.filename
3159 if ! save_current_file_user
3163 $config['last-opens'] ||= []
3164 $config['last-opens'] << $orig_filename
3170 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3172 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3173 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3174 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3176 dialog.vbox.add(notebook = Gtk::Notebook.new)
3177 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3178 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3179 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3180 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer']).set_size_request(250, -1)),
3181 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3182 tooltips = Gtk::Tooltips.new
3183 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3184 for example: /usr/bin/mplayer %f")), nil)
3185 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3186 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3187 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3188 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3189 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3190 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3191 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3192 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3193 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)),
3194 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3195 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)
3196 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3197 0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3198 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)
3199 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3200 0, 2, 5, 6, Gtk::FILL, Gtk::SHRINK, 2, 2)
3201 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)
3203 smp_check.signal_connect('toggled') {
3204 if smp_check.active?
3205 smp_hbox.sensitive = true
3207 smp_hbox.sensitive = false
3211 smp_check.active = true
3212 smp_spin.value = $config['mproc'].to_i
3214 nogestures_check.active = $config['nogestures']
3215 deleteondisk_check.active = $config['deleteondisk']
3217 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3218 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3219 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3220 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3221 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3222 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3223 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3224 tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3225 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3226 tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3227 2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3228 tooltips.set_tip(commentsformat_entry, utf8(_("Normally, filenames without extension are used as comments for images and videos in new albums. Use this entry to use something else for images.")), nil)
3229 commentsformat_help.signal_connect('clicked') {
3230 show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3231 hence you should look at ImageMagick/identify documentation for the most
3232 accurate and up-to-date documentation. Last time I checked, documentation
3235 Print information about the image in a format of your choosing. You can
3236 include the image filename, type, width, height, Exif data, or other image
3237 attributes by embedding special format characters:
3240 %P page width and height
3244 %e filename extension
3249 %k number of unique colors
3256 %r image class and colorspace
3259 %u unique temporary filename
3272 displays MIFF:bird.miff 512x480 for an image titled bird.miff and whose
3273 width is 512 and height is 480.
3275 If the first character of string is @, the format is read from a file titled
3276 by the remaining characters in the string.
3278 You can also use the following special formatting syntax to print Exif
3279 information contained in the file:
3283 Where tag can be one of the following:
3285 * (print all Exif tags, in keyword=data format)
3286 ! (print all Exif tags, in tag_number data format)
3287 #hhhh (print data for Exif tag #hhhh)
3292 PhotometricInterpretation
3312 PrimaryChromaticities
3315 JPEGInterchangeFormat
3316 JPEGInterchangeFormatLength
3338 ComponentsConfiguration
3339 CompressedBitsPerPixel
3359 InteroperabilityOffset
3361 SpatialFrequencyResponse
3362 FocalPlaneXResolution
3363 FocalPlaneYResolution
3364 FocalPlaneResolutionUnit
3369 SceneType")), { :scrolled => true })
3372 dialog.vbox.show_all
3373 dialog.run { |response|
3374 if response == Gtk::Dialog::RESPONSE_OK
3375 $config['video-viewer'] = video_viewer_entry.text
3376 $config['browser'] = browser_entry.text
3377 if smp_check.active?
3378 $config['mproc'] = smp_spin.value.to_i
3380 $config.delete('mproc')
3382 $config['nogestures'] = nogestures_check.active?
3383 $config['deleteondisk'] = deleteondisk_check.active?
3385 $config['convert-enhance'] = enhance_entry.text
3386 $config['comments-format'] = commentsformat_entry.text.gsub(/'/, '')