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-2008 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
27 require 'booh/libadds'
28 require 'booh/GtkAutoTable'
32 bindtextdomain("booh")
34 require 'booh/rexml/document'
37 require 'booh/booh-lib'
39 require 'booh/UndoHandler'
44 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
45 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
46 [ '--version', '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
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|
75 puts _("Booh version %s
77 Copyright (c) 2005-2008 Guillaume Cottenceau.
78 This is free software; see the source for copying conditions. There is NO
79 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
83 when '--verbose-level'
84 $verbose_level = arg.to_i
97 $config_file = File.expand_path('~/.booh-gui-rc')
98 if File.readable?($config_file)
99 xmldoc = REXML::Document.new(File.new($config_file))
100 xmldoc.root.elements.each { |element|
101 txt = element.get_text
103 if txt.value =~ /~~~/ || element.name == 'last-opens'
104 $config[element.name] = txt.value.split(/~~~/)
106 $config[element.name] = txt.value
108 elsif element.elements.size == 0
109 $config[element.name] = ''
111 $config[element.name] = {}
112 element.each { |chld|
114 $config[element.name][chld.name] = txt ? txt.value : nil
119 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
120 $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
121 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
122 $config['comments-format'] ||= '%t'
123 if !FileTest.directory?(File.expand_path('~/.booh'))
124 system("mkdir ~/.booh")
126 if $config['mproc'].nil?
128 for line in IO.readlines('/proc/cpuinfo') do
129 line =~ /^processor/ and cpus += 1
132 $config['mproc'] = cpus
135 $config['rotate-set-exif'] ||= 'true'
141 if !system("which convert >/dev/null 2>/dev/null")
142 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
143 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
146 if !system("which identify >/dev/null 2>/dev/null")
147 show_popup($main_window, utf8(_("The program 'identify' is needed to get photos sizes and EXIF data. Please install it.
148 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
150 if !system("which exif >/dev/null 2>/dev/null")
151 show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
153 missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
155 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
158 viewer_binary = $config['video-viewer'].split.first
159 if viewer_binary && !File.executable?(viewer_binary)
160 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
161 You should fix this in Edit/Preferences so that you can view videos.
163 Problem was: '%s' is not an executable file.
164 Hint: don't forget to specify the full path to the executable,
165 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
169 def check_image_editor
170 image_editor_binary = $config['image-editor'].split.first
171 if image_editor_binary && !File.executable?(image_editor_binary)
172 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
173 You should fix this in Edit/Preferences so that you can edit photos externally.
175 Problem was: '%s' is not an executable file.
176 Hint: don't forget to specify the full path to the executable,
177 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
185 if $config['last-opens'] && $config['last-opens'].size > 10
186 $config['last-opens'] = $config['last-opens'][-10, 10]
189 xmldoc = Document.new("<booh-gui-rc version='#{$VERSION}'/>")
190 xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
191 $config.each_pair { |key, value|
192 elem = xmldoc.root.add_element key
194 $config[key].each_pair { |subkey, subvalue|
195 subelem = elem.add_element subkey
196 subelem.add_text subvalue.to_s
198 elsif value.is_a? Array
199 elem.add_text value.join('~~~')
204 elem.add_text value.to_s
208 ios = File.open($config_file, "w")
212 $tempfiles.each { |f|
219 def set_mousecursor(what, *widget)
220 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
221 if widget[0] && widget[0].window
222 widget[0].window.cursor = cursor
224 if $main_window && $main_window.window
225 $main_window.window.cursor = cursor
227 $current_cursor = what
229 def set_mousecursor_wait(*widget)
230 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
231 if Thread.current == Thread.main
232 Gtk.main_iteration while Gtk.events_pending?
235 def set_mousecursor_normal(*widget)
236 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
238 def push_mousecursor_wait(*widget)
239 if $current_cursor != Gdk::Cursor::WATCH
240 $save_cursor = $current_cursor
241 gtk_thread_protect { set_mousecursor_wait(*widget) }
244 def pop_mousecursor(*widget)
245 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
249 source = $xmldoc.root.attributes['source']
250 dest = $xmldoc.root.attributes['destination']
251 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
254 def full_src_dir_to_rel(path, source)
255 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
258 def build_full_dest_filename(filename)
259 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
262 def save_undo(name, closure, *params)
263 UndoHandler.save_undo(name, closure, [ *params ])
264 $undo_tb.sensitive = $undo_mb.sensitive = true
265 $redo_tb.sensitive = $redo_mb.sensitive = false
268 def view_element(filename, closures)
269 if entry2type(filename) == 'video'
270 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
276 w = create_window.set_title(filename)
278 msg 3, "filename: #{filename}"
279 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
280 #- typically this file won't exist in case of videos; try with the largest thumbnail around
281 if !File.exists?(dest_img)
282 if entry2type(filename) == 'video'
283 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
284 if not alternatives.empty?
285 dest_img = alternatives[-1]
288 push_mousecursor_wait
289 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
291 if !File.exists?(dest_img)
292 msg 2, _("Could not generate fullscreen thumbnail!")
297 aspect = utf8(_("Aspect: unknown"))
298 size = get_image_size(from_utf8("#{$current_path}/#{filename}"))
300 aspect = utf8(_("Aspect: %s") % sprintf("%1.3f", size[:x].to_f/size[:y]))
302 vbox = Gtk::VBox.new.add(Gtk::Image.new(dest_img)).add(Gtk::Label.new.set_markup("<i>#{aspect}</i>"))
303 evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(vbox).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
304 evt.signal_connect('button-press-event') { |this, event|
305 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
306 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
308 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
310 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
311 delete_item.signal_connect('activate') {
313 closures[:delete].call(false)
316 menu.popup(nil, nil, event.button, event.time)
319 evt.signal_connect('button-release-event') { |this, event|
321 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
322 msg 3, "gesture delete: click-drag right button to the bottom"
324 closures[:delete].call(false)
325 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
329 tooltips = Gtk::Tooltips.new
330 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
332 w.signal_connect('key-press-event') { |w,event|
333 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
335 closures[:delete].call(false)
339 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
340 b.signal_connect('clicked') { w.destroy }
343 vb.pack_start(evt, false, false)
344 vb.pack_end(bottom, false, false)
347 w.signal_connect('delete-event') { w.destroy }
348 w.window_position = Gtk::Window::POS_CENTER
352 def scroll_upper(scrolledwindow, ypos_top)
353 newval = scrolledwindow.vadjustment.value -
354 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
355 if newval < scrolledwindow.vadjustment.lower
356 newval = scrolledwindow.vadjustment.lower
358 scrolledwindow.vadjustment.value = newval
361 def scroll_lower(scrolledwindow, ypos_bottom)
362 newval = scrolledwindow.vadjustment.value +
363 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
364 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
365 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
367 scrolledwindow.vadjustment.value = newval
370 def autoscroll_if_needed(scrolledwindow, image, textview)
371 #- autoscroll if cursor or image is not visible, if possible
372 if image && image.window || textview.window
373 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
374 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
375 current_miny_visible = scrolledwindow.vadjustment.value
376 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
377 if ypos_top < current_miny_visible
378 scroll_upper(scrolledwindow, ypos_top)
379 elsif ypos_bottom > current_maxy_visible
380 scroll_lower(scrolledwindow, ypos_bottom)
385 def create_editzone(scrolledwindow, pagenum, image)
386 frame = Gtk::Frame.new
387 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
388 frame.set_shadow_type(Gtk::SHADOW_IN)
389 textview.signal_connect('key-press-event') { |w, event|
390 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
391 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
392 scrolledwindow.signal_emit('key-press-event', event)
394 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
395 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
396 if event.keyval == Gdk::Keyval::GDK_Up
397 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
398 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
400 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
403 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
404 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
406 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
413 candidate_undo_text = nil
414 textview.signal_connect('focus-in-event') { |w, event|
415 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
416 candidate_undo_text = textview.buffer.text
420 textview.signal_connect('key-release-event') { |w, event|
421 if candidate_undo_text && candidate_undo_text != textview.buffer.text
423 save_undo(_("text edit"),
425 save_text = textview.buffer.text
426 textview.buffer.text = text
428 $notebook.set_page(pagenum)
430 textview.buffer.text = save_text
432 $notebook.set_page(pagenum)
434 }, candidate_undo_text)
435 candidate_undo_text = nil
438 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)
439 autoscroll_if_needed(scrolledwindow, image, textview)
444 return [ frame, textview ]
447 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
449 if !$modified_pixbufs[thumbnail_img]
450 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
451 elsif !$modified_pixbufs[thumbnail_img][:orig]
452 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
455 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
458 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
459 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
460 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
461 if pixbuf.height > desired_y
462 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
463 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
464 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
469 if $modified_pixbufs[thumbnail_img][:whitebalance]
470 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
473 #- fix gamma correction
474 if $modified_pixbufs[thumbnail_img][:gammacorrect]
475 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
478 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
481 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
484 #- update rotate attribute
485 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
486 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
488 #- change exif orientation if configured so (but forget in case of thumbnails caption)
489 if $config['rotate-set-exif'] == 'true' && xmlelem.attributes['filename']
490 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
493 $modified_pixbufs[thumbnail_img] ||= {}
494 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
495 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
497 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
500 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
503 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
505 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
507 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
508 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
510 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
511 $notebook.set_page(0)
512 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
517 def color_swap(xmldir, attributes_prefix)
519 rexml_thread_protect {
520 if xmldir.attributes["#{attributes_prefix}color-swap"]
521 xmldir.delete_attribute("#{attributes_prefix}color-swap")
523 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
528 def enhance(xmldir, attributes_prefix)
530 rexml_thread_protect {
531 if xmldir.attributes["#{attributes_prefix}enhance"]
532 xmldir.delete_attribute("#{attributes_prefix}enhance")
534 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
539 def change_seektime(xmldir, attributes_prefix, value)
541 rexml_thread_protect {
542 xmldir.add_attribute("#{attributes_prefix}seektime", value)
546 def ask_new_seektime(xmldir, attributes_prefix)
547 value = rexml_thread_protect {
549 xmldir.attributes["#{attributes_prefix}seektime"]
555 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
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>seek time</b> of the video, to take the thumbnail
567 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
568 entry.signal_connect('key-press-event') { |w, event|
569 if event.keyval == Gdk::Keyval::GDK_Return
570 dialog.response(Gtk::Dialog::RESPONSE_OK)
572 elsif event.keyval == Gdk::Keyval::GDK_Escape
573 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
576 false #- propagate if needed
580 dialog.window_position = Gtk::Window::POS_MOUSE
583 dialog.run { |response|
586 if response == Gtk::Dialog::RESPONSE_OK
588 msg 3, "changing seektime to #{newval}"
589 return { :old => value, :new => newval }
596 def change_pano_amount(xmldir, attributes_prefix, value)
598 rexml_thread_protect {
600 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
602 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
607 def ask_new_pano_amount(xmldir, attributes_prefix)
608 value = rexml_thread_protect {
610 xmldir.attributes["#{attributes_prefix}pano-amount"]
616 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
618 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
619 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
620 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
624 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
625 of this panorama image compared to other regular images. For example, if the panorama
626 was taken out of four photos on one row, counting the necessary overlap, the width of
627 this panorama image should probably be roughly three times the width of regular images.
629 With this information, booh will be able to generate panorama thumbnails looking
630 the right 'size', since the height of the thumbnail for this image will be similar
631 to the height of other thumbnails.
634 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)")))).
635 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
636 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
637 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
638 spin.signal_connect('value-changed') {
641 dialog.window_position = Gtk::Window::POS_MOUSE
644 spin.value = value.to_f
651 dialog.run { |response|
655 newval = spin.value.to_f
658 if response == Gtk::Dialog::RESPONSE_OK
660 msg 3, "changing panorama amount to #{newval}"
661 return { :old => value, :new => newval }
668 def change_whitebalance(xmlelem, attributes_prefix, value)
670 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
673 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
675 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
676 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
677 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
678 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
679 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
680 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
681 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
682 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
683 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
684 $modified_pixbufs[thumbnail_img] ||= {}
685 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
686 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
688 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
689 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
691 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
694 $modified_pixbufs[thumbnail_img] ||= {}
695 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
697 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
700 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
701 #- init $modified_pixbufs correctly
702 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
704 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
706 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
708 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
709 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
710 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
714 _("You can fix the <b>white balance</b> of the image, if your image is too blue
715 or too yellow because the recorder didn't detect the light correctly. Drag the
716 slider below the image to the left for more blue, to the right for more yellow.
720 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
722 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
724 dialog.window_position = Gtk::Window::POS_MOUSE
728 timeout = Gtk.timeout_add(100) {
729 if hs.value != lastval
732 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
738 dialog.run { |response|
739 Gtk.timeout_remove(timeout)
740 if response == Gtk::Dialog::RESPONSE_OK
742 newval = hs.value.to_s
743 msg 3, "changing white balance to #{newval}"
745 return { :old => value, :new => newval }
748 $modified_pixbufs[thumbnail_img] ||= {}
749 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
750 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
758 def change_gammacorrect(xmlelem, attributes_prefix, value)
760 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
763 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
765 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
766 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
767 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
768 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
769 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
770 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
771 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
772 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
773 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
774 $modified_pixbufs[thumbnail_img] ||= {}
775 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
776 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
778 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
779 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
781 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
784 $modified_pixbufs[thumbnail_img] ||= {}
785 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
787 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
790 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
791 #- init $modified_pixbufs correctly
792 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
794 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
796 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
798 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
799 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
800 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
804 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
805 or too bright. Drag the slider below the image.
809 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
811 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
813 dialog.window_position = Gtk::Window::POS_MOUSE
817 timeout = Gtk.timeout_add(100) {
818 if hs.value != lastval
821 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
827 dialog.run { |response|
828 Gtk.timeout_remove(timeout)
829 if response == Gtk::Dialog::RESPONSE_OK
831 newval = hs.value.to_s
832 msg 3, "gamma correction to #{newval}"
834 return { :old => value, :new => newval }
837 $modified_pixbufs[thumbnail_img] ||= {}
838 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
839 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
847 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
848 if File.exists?(destfile)
849 File.delete(destfile)
851 #- type can be 'element' or 'subdir'
853 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
855 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
859 $max_gen_thumbnail_threads = nil
860 $current_gen_thumbnail_threads = 0
861 $gen_thumbnail_monitor = Monitor.new
863 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
864 if $max_gen_thumbnail_threads.nil?
865 $max_gen_thumbnail_threads = 1 + $config['mproc'].to_i || 1
868 push_mousecursor_wait
869 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
872 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
877 $gen_thumbnail_monitor.synchronize {
878 if $current_gen_thumbnail_threads < $max_gen_thumbnail_threads
879 $current_gen_thumbnail_threads += 1
884 msg 3, "generate thumbnail from new thread"
887 $gen_thumbnail_monitor.synchronize {
888 $current_gen_thumbnail_threads -= 1
892 msg 3, "generate thumbnail from current thread"
897 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
898 distribute_multiple_call = Proc.new { |action, arg|
899 $selected_elements.each_key { |path|
900 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
902 if possible_actions[:can_multiple] && $selected_elements.length > 0
903 UndoHandler.begin_batch
904 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
905 UndoHandler.end_batch
907 closures[action].call(arg)
909 $selected_elements = {}
912 if optionals.include?('change_image')
913 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
914 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
915 changeimg.signal_connect('activate') { closures[:change].call }
916 menu.append(Gtk::SeparatorMenuItem.new)
918 if !possible_actions[:can_multiple] || $selected_elements.length == 0
921 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
922 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
923 view.signal_connect('activate') { closures[:view].call }
925 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
926 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
927 view.signal_connect('activate') { closures[:view].call }
928 menu.append(Gtk::SeparatorMenuItem.new)
931 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
932 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
933 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
934 exif.signal_connect('activate') { show_popup($main_window,
935 utf8(`exif -m '#{fullpath}'`),
936 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
937 menu.append(Gtk::SeparatorMenuItem.new)
940 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
941 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
942 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
943 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
944 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
945 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
946 if !possible_actions[:can_multiple] || $selected_elements.length == 0
947 menu.append(Gtk::SeparatorMenuItem.new)
948 if !possible_actions[:forbid_left]
949 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
950 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
951 moveleft.signal_connect('activate') { closures[:move].call('left') }
952 if !possible_actions[:can_left]
953 moveleft.sensitive = false
956 if !possible_actions[:forbid_right]
957 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
958 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
959 moveright.signal_connect('activate') { closures[:move].call('right') }
960 if !possible_actions[:can_right]
961 moveright.sensitive = false
964 if optionals.include?('move_top')
965 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
966 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
967 movetop.signal_connect('activate') { closures[:move].call('top') }
968 if !possible_actions[:can_top]
969 movetop.sensitive = false
972 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
973 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
974 moveup.signal_connect('activate') { closures[:move].call('up') }
975 if !possible_actions[:can_up]
976 moveup.sensitive = false
978 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
979 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
980 movedown.signal_connect('activate') { closures[:move].call('down') }
981 if !possible_actions[:can_down]
982 movedown.sensitive = false
984 if optionals.include?('move_bottom')
985 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
986 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
987 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
988 if !possible_actions[:can_bottom]
989 movebottom.sensitive = false
994 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
995 menu.append(Gtk::SeparatorMenuItem.new)
996 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
997 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
998 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
999 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
1000 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
1001 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
1002 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
1003 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
1004 seektime.signal_connect('activate') {
1005 if possible_actions[:can_multiple] && $selected_elements.length > 0
1006 if values = ask_new_seektime(nil, '')
1007 distribute_multiple_call.call(:seektime, values)
1010 closures[:seektime].call
1015 menu.append( Gtk::SeparatorMenuItem.new)
1016 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
1017 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
1018 gammacorrect.signal_connect('activate') {
1019 if possible_actions[:can_multiple] && $selected_elements.length > 0
1020 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
1021 distribute_multiple_call.call(:gammacorrect, values)
1024 closures[:gammacorrect].call
1027 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
1028 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
1029 whitebalance.signal_connect('activate') {
1030 if possible_actions[:can_multiple] && $selected_elements.length > 0
1031 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
1032 distribute_multiple_call.call(:whitebalance, values)
1035 closures[:whitebalance].call
1038 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1039 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(rexml_thread_protect { xmldir.attributes["#{attributes_prefix}enhance"] } ? _("Original contrast") :
1040 _("Enhance constrast"))))
1042 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
1044 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1045 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
1046 if type == 'image' && possible_actions[:can_panorama]
1047 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
1048 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
1049 panorama.signal_connect('activate') {
1050 if possible_actions[:can_multiple] && $selected_elements.length > 0
1051 if values = ask_new_pano_amount(nil, '')
1052 distribute_multiple_call.call(:pano, values)
1055 distribute_multiple_call.call(:pano)
1059 menu.append( Gtk::SeparatorMenuItem.new)
1060 if optionals.include?('delete')
1061 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1062 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1063 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1064 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1065 paste_item.signal_connect('activate') { closures[:paste].call }
1066 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1067 clear_item.signal_connect('activate') { $cuts = [] }
1069 paste_item.sensitive = clear_item.sensitive = false
1072 menu.append( Gtk::SeparatorMenuItem.new)
1074 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1075 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1076 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1077 editexternally.signal_connect('activate') {
1078 if check_image_editor
1079 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1085 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1086 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1087 if optionals.include?('delete')
1088 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1089 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1092 menu.popup(nil, nil, event.button, event.time)
1095 def delete_current_subalbum
1097 sel = $albums_tv.selection.selected_rows
1098 $xmldir.elements.each { |e|
1099 if e.name == 'image' || e.name == 'video'
1100 e.add_attribute('deleted', 'true')
1103 #- branch if we have a non deleted subalbum
1104 if $xmldir.child_byname_notattr('dir', 'deleted')
1105 $xmldir.delete_attribute('thumbnails-caption')
1106 $xmldir.delete_attribute('thumbnails-captionfile')
1108 $xmldir.add_attribute('deleted', 'true')
1110 while moveup.parent.name == 'dir'
1111 moveup = moveup.parent
1112 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1113 moveup.add_attribute('deleted', 'true')
1120 save_changes('forced')
1121 populate_subalbums_treeview(false)
1122 $albums_tv.selection.select_path(sel[0])
1128 $current_path = nil #- prevent save_changes from being rerun again
1129 sel = $albums_tv.selection.selected_rows
1130 restore_one = proc { |xmldir|
1131 xmldir.elements.each { |e|
1132 if e.name == 'dir' && e.attributes['deleted']
1135 e.delete_attribute('deleted')
1138 restore_one.call($xmldir)
1139 populate_subalbums_treeview(false)
1140 $albums_tv.selection.select_path(sel[0])
1143 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1146 frame1 = Gtk::Frame.new
1147 fullpath = from_utf8("#{$current_path}/#{filename}")
1149 my_gen_real_thumbnail = proc {
1150 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1154 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1155 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1156 pack_start(img = Gtk::Image.new).
1157 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1158 px, mask = pxb.render_pixmap_and_mask(0)
1159 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1160 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1162 frame1.add(img = Gtk::Image.new)
1165 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1166 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1167 my_gen_real_thumbnail.call
1169 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1172 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1174 tooltips = Gtk::Tooltips.new
1175 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1176 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1178 frame2, textview = create_editzone($autotable_sw, 1, img)
1179 textview.buffer.text = caption
1180 textview.set_justification(Gtk::Justification::CENTER)
1182 vbox = Gtk::VBox.new(false, 5)
1183 vbox.pack_start(evtbox, false, false)
1184 vbox.pack_start(frame2, false, false)
1185 autotable.append(vbox, filename)
1187 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1188 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1190 #- to be able to find widgets by name
1191 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1193 cleanup_all_thumbnails = proc {
1194 #- remove out of sync images
1195 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1196 for sizeobj in $images_size
1197 #- cannot use sizeobj because panoramic images will have a larger width
1198 Dir.glob("#{dest_img_base}-*.jpg") do |file|
1206 cleanup_all_thumbnails.call
1207 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1209 rexml_thread_protect {
1210 $xmldir.delete_attribute('already-generated')
1212 my_gen_real_thumbnail.call
1215 rotate_and_cleanup = proc { |angle|
1216 cleanup_all_thumbnails.call
1217 rexml_thread_protect {
1218 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1222 move = proc { |direction|
1223 do_method = "move_#{direction}"
1224 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1226 done = autotable.method(do_method).call(vbox)
1227 textview.grab_focus #- because if moving, focus is stolen
1231 save_undo(_("move %s") % direction,
1233 autotable.method(undo_method).call(vbox)
1234 textview.grab_focus #- because if moving, focus is stolen
1235 autoscroll_if_needed($autotable_sw, img, textview)
1236 $notebook.set_page(1)
1238 autotable.method(do_method).call(vbox)
1239 textview.grab_focus #- because if moving, focus is stolen
1240 autoscroll_if_needed($autotable_sw, img, textview)
1241 $notebook.set_page(1)
1247 color_swap_and_cleanup = proc {
1248 perform_color_swap_and_cleanup = proc {
1249 cleanup_all_thumbnails.call
1250 rexml_thread_protect {
1251 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1253 my_gen_real_thumbnail.call
1256 perform_color_swap_and_cleanup.call
1258 save_undo(_("color swap"),
1260 perform_color_swap_and_cleanup.call
1262 autoscroll_if_needed($autotable_sw, img, textview)
1263 $notebook.set_page(1)
1265 perform_color_swap_and_cleanup.call
1267 autoscroll_if_needed($autotable_sw, img, textview)
1268 $notebook.set_page(1)
1273 change_seektime_and_cleanup_real = proc { |values|
1274 perform_change_seektime_and_cleanup = proc { |val|
1275 cleanup_all_thumbnails.call
1276 rexml_thread_protect {
1277 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1279 my_gen_real_thumbnail.call
1281 perform_change_seektime_and_cleanup.call(values[:new])
1283 save_undo(_("specify seektime"),
1285 perform_change_seektime_and_cleanup.call(values[:old])
1287 autoscroll_if_needed($autotable_sw, img, textview)
1288 $notebook.set_page(1)
1290 perform_change_seektime_and_cleanup.call(values[:new])
1292 autoscroll_if_needed($autotable_sw, img, textview)
1293 $notebook.set_page(1)
1298 change_seektime_and_cleanup = proc {
1299 rexml_thread_protect {
1300 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1301 change_seektime_and_cleanup_real.call(values)
1306 change_pano_amount_and_cleanup_real = proc { |values|
1307 perform_change_pano_amount_and_cleanup = proc { |val|
1308 cleanup_all_thumbnails.call
1309 rexml_thread_protect {
1310 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1313 perform_change_pano_amount_and_cleanup.call(values[:new])
1315 save_undo(_("change panorama amount"),
1317 perform_change_pano_amount_and_cleanup.call(values[:old])
1319 autoscroll_if_needed($autotable_sw, img, textview)
1320 $notebook.set_page(1)
1322 perform_change_pano_amount_and_cleanup.call(values[:new])
1324 autoscroll_if_needed($autotable_sw, img, textview)
1325 $notebook.set_page(1)
1330 change_pano_amount_and_cleanup = proc {
1331 rexml_thread_protect {
1332 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1333 change_pano_amount_and_cleanup_real.call(values)
1338 whitebalance_and_cleanup_real = proc { |values|
1339 perform_change_whitebalance_and_cleanup = proc { |val|
1340 cleanup_all_thumbnails.call
1341 rexml_thread_protect {
1342 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1343 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1344 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1347 perform_change_whitebalance_and_cleanup.call(values[:new])
1349 save_undo(_("fix white balance"),
1351 perform_change_whitebalance_and_cleanup.call(values[:old])
1353 autoscroll_if_needed($autotable_sw, img, textview)
1354 $notebook.set_page(1)
1356 perform_change_whitebalance_and_cleanup.call(values[:new])
1358 autoscroll_if_needed($autotable_sw, img, textview)
1359 $notebook.set_page(1)
1364 whitebalance_and_cleanup = proc {
1365 rexml_thread_protect {
1366 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1367 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1368 whitebalance_and_cleanup_real.call(values)
1373 gammacorrect_and_cleanup_real = proc { |values|
1374 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1375 cleanup_all_thumbnails.call
1376 rexml_thread_protect {
1377 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1378 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1379 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1382 perform_change_gammacorrect_and_cleanup.call(values[:new])
1384 save_undo(_("gamma correction"),
1386 perform_change_gammacorrect_and_cleanup.call(values[:old])
1388 autoscroll_if_needed($autotable_sw, img, textview)
1389 $notebook.set_page(1)
1391 perform_change_gammacorrect_and_cleanup.call(values[:new])
1393 autoscroll_if_needed($autotable_sw, img, textview)
1394 $notebook.set_page(1)
1399 gammacorrect_and_cleanup = Proc.new {
1400 rexml_thread_protect {
1401 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1402 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1403 gammacorrect_and_cleanup_real.call(values)
1408 enhance_and_cleanup = proc {
1409 perform_enhance_and_cleanup = proc {
1410 cleanup_all_thumbnails.call
1411 rexml_thread_protect {
1412 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1414 my_gen_real_thumbnail.call
1417 cleanup_all_thumbnails.call
1418 perform_enhance_and_cleanup.call
1420 save_undo(_("enhance"),
1422 perform_enhance_and_cleanup.call
1424 autoscroll_if_needed($autotable_sw, img, textview)
1425 $notebook.set_page(1)
1427 perform_enhance_and_cleanup.call
1429 autoscroll_if_needed($autotable_sw, img, textview)
1430 $notebook.set_page(1)
1435 delete = proc { |isacut|
1436 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 })
1439 perform_delete = proc {
1440 after = autotable.get_next_widget(vbox)
1442 after = autotable.get_previous_widget(vbox)
1444 if $config['deleteondisk'] && !isacut
1445 msg 3, "scheduling for delete: #{fullpath}"
1446 $todelete << fullpath
1448 autotable.remove_widget(vbox)
1450 $vbox2widgets[after][:textview].grab_focus
1451 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1455 previous_pos = autotable.get_current_number(vbox)
1459 delete_current_subalbum
1461 save_undo(_("delete"),
1463 autotable.reinsert(pos, vbox, filename)
1464 $notebook.set_page(1)
1465 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1467 msg 3, "removing deletion schedule of: #{fullpath}"
1468 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1471 $notebook.set_page(1)
1480 $cuts << { :vbox => vbox, :filename => filename }
1481 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1486 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1489 autotable.queue_draws << proc {
1490 $vbox2widgets[last[:vbox]][:textview].grab_focus
1491 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1493 save_undo(_("paste"),
1495 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1496 $notebook.set_page(1)
1499 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1501 $notebook.set_page(1)
1504 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1509 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1510 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1511 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1512 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1514 textview.signal_connect('key-press-event') { |w, event|
1517 x, y = autotable.get_current_pos(vbox)
1518 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1519 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1520 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1521 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1523 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1524 $vbox2widgets[widget_up][:textview].grab_focus
1531 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1533 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1534 $vbox2widgets[widget_down][:textview].grab_focus
1541 if event.keyval == Gdk::Keyval::GDK_Left
1544 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1551 rotate_and_cleanup.call(-90)
1554 if event.keyval == Gdk::Keyval::GDK_Right
1555 next_ = autotable.get_next_widget(vbox)
1556 if next_ && autotable.get_current_pos(next_)[0] > x
1558 $vbox2widgets[next_][:textview].grab_focus
1565 rotate_and_cleanup.call(90)
1568 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1571 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1572 view_element(filename, { :delete => delete })
1575 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1578 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1582 !propagate #- propagate if needed
1585 $ignore_next_release = false
1586 evtbox.signal_connect('button-press-event') { |w, event|
1587 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1588 if event.state & Gdk::Window::BUTTON3_MASK != 0
1589 #- gesture redo: hold right mouse button then click left mouse button
1590 $config['nogestures'] or perform_redo
1591 $ignore_next_release = true
1593 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1595 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1597 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1598 elsif $enhance.active?
1599 enhance_and_cleanup.call
1600 elsif $delete.active?
1604 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1607 $button1_pressed_autotable = true
1608 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1609 if event.state & Gdk::Window::BUTTON1_MASK != 0
1610 #- gesture undo: hold left mouse button then click right mouse button
1611 $config['nogestures'] or perform_undo
1612 $ignore_next_release = true
1614 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1615 view_element(filename, { :delete => delete })
1620 evtbox.signal_connect('button-release-event') { |w, event|
1621 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1622 if !$ignore_next_release
1623 x, y = autotable.get_current_pos(vbox)
1624 next_ = autotable.get_next_widget(vbox)
1625 popup_thumbnail_menu(event, ['delete'], fullpath, type, rexml_thread_protect { $xmldir.elements["*[@filename='#{filename}']"] }, '',
1626 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1627 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1628 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1629 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1630 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1631 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1633 $ignore_next_release = false
1634 $gesture_press = nil
1639 #- handle reordering with drag and drop
1640 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1641 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1642 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1643 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1646 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1648 #- mouse gesture first (dnd disables button-release-event)
1649 if $gesture_press && $gesture_press[:filename] == filename
1650 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1651 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1652 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1653 rotate_and_cleanup.call(angle)
1654 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1656 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1657 msg 3, "gesture delete: click-drag right button to the bottom"
1659 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1664 ctxt.targets.each { |target|
1665 if target.name == 'reorder-elements'
1666 move_dnd = proc { |from,to|
1669 autotable.move(from, to)
1670 save_undo(_("reorder"),
1673 autotable.move(to - 1, from)
1675 autotable.move(to, from + 1)
1677 $notebook.set_page(1)
1679 autotable.move(from, to)
1680 $notebook.set_page(1)
1685 if $multiple_dnd.size == 0
1686 move_dnd.call(selection_data.data.to_i,
1687 autotable.get_current_number(vbox))
1689 UndoHandler.begin_batch
1690 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1692 #- need to update current position between each call
1693 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1694 autotable.get_current_number(vbox))
1696 UndoHandler.end_batch
1707 def create_auto_table
1709 $autotable = Gtk::AutoTable.new(5)
1711 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1712 thumbnails_vb = Gtk::VBox.new(false, 5)
1714 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1715 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1716 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1717 thumbnails_vb.add($autotable)
1719 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1720 $autotable_sw.add_with_viewport(thumbnails_vb)
1722 #- follows stuff for handling multiple elements selection
1723 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1725 update_selected = proc {
1726 $autotable.current_order.each { |path|
1727 w = $name2widgets[path][:evtbox].window
1728 xm = w.position[0] + w.size[0]/2
1729 ym = w.position[1] + w.size[1]/2
1730 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1731 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1732 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1733 if $name2widgets[path][:img].pixbuf
1734 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1738 if $selected_elements[path] && ! $selected_elements[path][:keep]
1739 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))
1740 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1741 $selected_elements.delete(path)
1746 $autotable.signal_connect('realize') { |w,e|
1747 gc = Gdk::GC.new($autotable.window)
1748 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1749 gc.function = Gdk::GC::INVERT
1750 #- autoscroll handling for DND and multiple selections
1751 Gtk.timeout_add(100) {
1752 if ! $autotable.window.nil?
1753 w, x, y, mask = $autotable.window.pointer
1754 if mask & Gdk::Window::BUTTON1_MASK != 0
1755 if y < $autotable_sw.vadjustment.value
1757 $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]])
1759 if $button1_pressed_autotable || press_x
1760 scroll_upper($autotable_sw, y)
1763 w, pos_x, pos_y = $autotable.window.pointer
1764 $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]])
1765 update_selected.call
1768 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1770 $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]])
1772 if $button1_pressed_autotable || press_x
1773 scroll_lower($autotable_sw, y)
1776 w, pos_x, pos_y = $autotable.window.pointer
1777 $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]])
1778 update_selected.call
1783 ! $autotable.window.nil?
1787 $autotable.signal_connect('button-press-event') { |w,e|
1789 if !$button1_pressed_autotable
1792 if e.state & Gdk::Window::SHIFT_MASK == 0
1793 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1794 $selected_elements = {}
1795 $statusbar.push(0, utf8(_("Nothing selected.")))
1797 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1799 set_mousecursor(Gdk::Cursor::TCROSS)
1803 $autotable.signal_connect('button-release-event') { |w,e|
1805 if $button1_pressed_autotable
1806 #- unselect all only now
1807 $multiple_dnd = $selected_elements.keys
1808 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1809 $selected_elements = {}
1810 $button1_pressed_autotable = false
1813 $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]])
1814 if $selected_elements.length > 0
1815 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1818 press_x = press_y = pos_x = pos_y = nil
1819 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1823 $autotable.signal_connect('motion-notify-event') { |w,e|
1826 $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]])
1830 $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]])
1831 update_selected.call
1837 def create_subalbums_page
1839 subalbums_hb = Gtk::HBox.new
1840 $subalbums_vb = Gtk::VBox.new(false, 5)
1841 subalbums_hb.pack_start($subalbums_vb, false, false)
1842 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1843 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1844 $subalbums_sw.add_with_viewport(subalbums_hb)
1847 def save_current_file
1853 ios = File.open($filename, "w")
1854 $xmldoc.write(ios, 0)
1856 rescue Iconv::IllegalSequence
1857 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1858 if ! ios.nil? && ! ios.closed?
1861 $xmldoc.xml_decl.encoding = 'UTF-8'
1862 ios = File.open($filename, "w")
1863 $xmldoc.write(ios, 0)
1874 def save_current_file_user
1875 save_tempfilename = $filename
1876 $filename = $orig_filename
1877 if ! save_current_file
1878 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1879 $filename = save_tempfilename
1883 $generated_outofline = false
1884 $filename = save_tempfilename
1886 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1887 $todelete.each { |f|
1892 def mark_document_as_dirty
1893 $xmldoc.elements.each('//dir') { |elem|
1894 elem.delete_attribute('already-generated')
1898 #- ret: true => ok false => cancel
1899 def ask_save_modifications(msg1, msg2, *options)
1901 options = options.size > 0 ? options[0] : {}
1903 if options[:disallow_cancel]
1904 dialog = Gtk::Dialog.new(msg1,
1906 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1907 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1908 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1910 dialog = Gtk::Dialog.new(msg1,
1912 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1913 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1914 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1915 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1917 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1918 dialog.vbox.add(Gtk::Label.new(msg2))
1919 dialog.window_position = Gtk::Window::POS_CENTER
1922 dialog.run { |response|
1924 if response == Gtk::Dialog::RESPONSE_YES
1925 if ! save_current_file_user
1926 return ask_save_modifications(msg1, msg2, options)
1929 #- if we have generated an album but won't save modifications, we must remove
1930 #- already-generated markers in original file
1931 if $generated_outofline
1933 $xmldoc = REXML::Document.new(File.new($orig_filename))
1934 mark_document_as_dirty
1935 ios = File.open($orig_filename, "w")
1936 $xmldoc.write(ios, 0)
1939 puts "exception: #{$!}"
1943 if response == Gtk::Dialog::RESPONSE_CANCEL
1946 $todelete = [] #- unconditionally clear the list of images/videos to delete
1952 def try_quit(*options)
1953 if ask_save_modifications(utf8(_("Save before quitting?")),
1954 utf8(_("Do you want to save your changes before quitting?")),
1960 def show_popup(parent, msg, *options)
1961 dialog = Gtk::Dialog.new
1962 if options[0] && options[0][:title]
1963 dialog.title = options[0][:title]
1965 dialog.title = utf8(_("Booh message"))
1967 lbl = Gtk::Label.new
1968 if options[0] && options[0][:nomarkup]
1973 if options[0] && options[0][:centered]
1974 lbl.set_justify(Gtk::Justification::CENTER)
1976 if options[0] && options[0][:selectable]
1977 lbl.selectable = true
1979 if options[0] && options[0][:topwidget]
1980 dialog.vbox.add(options[0][:topwidget])
1982 if options[0] && options[0][:scrolled]
1983 sw = Gtk::ScrolledWindow.new(nil, nil)
1984 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1985 sw.add_with_viewport(lbl)
1987 dialog.set_default_size(500, 600)
1989 dialog.vbox.add(lbl)
1990 dialog.set_default_size(200, 120)
1992 if options[0] && options[0][:okcancel]
1993 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1995 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1997 if options[0] && options[0][:pos_centered]
1998 dialog.window_position = Gtk::Window::POS_CENTER
2000 dialog.window_position = Gtk::Window::POS_MOUSE
2003 if options[0] && options[0][:linkurl]
2004 linkbut = Gtk::Button.new('')
2005 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
2006 linkbut.signal_connect('clicked') {
2007 open_url(options[0][:linkurl])
2008 dialog.response(Gtk::Dialog::RESPONSE_OK)
2009 set_mousecursor_normal
2011 linkbut.relief = Gtk::RELIEF_NONE
2012 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
2013 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
2014 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
2019 if !options[0] || !options[0][:not_transient]
2020 dialog.transient_for = parent
2021 dialog.run { |response|
2023 if options[0] && options[0][:okcancel]
2024 return response == Gtk::Dialog::RESPONSE_OK
2028 dialog.signal_connect('response') { dialog.destroy }
2032 def set_mainwindow_title(progress)
2033 filename = $orig_filename || $filename
2036 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
2038 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
2042 $main_window.title = 'booh - ' + File.basename(filename)
2044 $main_window.title = 'booh'
2049 def backend_wait_message(parent, msg, infopipe_path, mode)
2051 w.set_transient_for(parent)
2054 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2055 vb.pack_start(Gtk::Label.new(msg), false, false)
2057 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
2058 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
2059 if mode != 'one dir scan'
2060 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2062 if mode == 'web-album'
2063 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2064 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2066 vb.pack_start(Gtk::HSeparator.new, false, false)
2068 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2069 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2070 vb.pack_end(bottom, false, false)
2073 update_progression_title_pb1 = proc {
2074 if mode == 'web-album'
2075 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2076 elsif mode != 'one dir scan'
2077 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2079 set_mainwindow_title(pb1_1.fraction)
2083 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2084 refresh_thread = Thread.new {
2085 directories_counter = 0
2086 while line = infopipe.gets
2087 msg 3, "infopipe got data: #{line}"
2088 if line =~ /^directories: (\d+), sizes: (\d+)/
2089 directories = $1.to_f + 1
2091 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2092 elements = $3.to_f + 1
2093 if mode == 'web-album'
2097 gtk_thread_protect { pb1_1.fraction = 0 }
2098 if mode != 'one dir scan'
2099 newtext = utf8(full_src_dir_to_rel($1, $2))
2100 newtext = '/' if newtext == ''
2101 gtk_thread_protect { pb1_2.text = newtext }
2102 directories_counter += 1
2103 gtk_thread_protect {
2104 pb1_2.fraction = directories_counter / directories
2105 update_progression_title_pb1.call
2108 elsif line =~ /^processing element$/
2109 element_counter += 1
2110 gtk_thread_protect {
2111 pb1_1.fraction = element_counter / elements
2112 update_progression_title_pb1.call
2114 elsif line =~ /^processing size$/
2115 element_counter += 1
2116 gtk_thread_protect {
2117 pb1_1.fraction = element_counter / elements
2118 update_progression_title_pb1.call
2120 elsif line =~ /^finished processing sizes$/
2121 gtk_thread_protect { pb1_1.fraction = 1 }
2122 elsif line =~ /^creating index.html$/
2123 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2124 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2125 directories_counter = 0
2126 elsif line =~ /^index.html: (.+)\|(.+)/
2127 newtext = utf8(full_src_dir_to_rel($1, $2))
2128 newtext = '/' if newtext == ''
2129 gtk_thread_protect { pb2.text = newtext }
2130 directories_counter += 1
2131 gtk_thread_protect {
2132 pb2.fraction = directories_counter / directories
2133 set_mainwindow_title(0.9 + pb2.fraction / 10)
2135 elsif line =~ /^die: (.*)$/
2142 w.signal_connect('delete-event') { w.destroy }
2143 w.signal_connect('destroy') {
2144 Thread.kill(refresh_thread)
2145 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2148 File.delete(infopipe_path)
2150 set_mainwindow_title(nil)
2152 w.window_position = Gtk::Window::POS_CENTER
2158 def call_backend(cmd, waitmsg, mode, params)
2159 pipe = Tempfile.new("boohpipe")
2160 Thread.critical = true
2163 system("mkfifo #{path}")
2164 Thread.critical = false
2165 cmd += " --info-pipe #{path}"
2166 button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2171 id, exitstatus = Process.waitpid2(pid)
2172 gtk_thread_protect { w8.destroy }
2174 if params[:successmsg]
2175 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2177 if params[:closure_after]
2178 gtk_thread_protect(¶ms[:closure_after])
2180 elsif exitstatus == 15
2181 #- say nothing, user aborted
2183 gtk_thread_protect { show_popup($main_window,
2184 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2190 button.signal_connect('clicked') {
2191 Process.kill('SIGTERM', pid)
2195 def save_changes(*forced)
2196 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2200 $xmldir.delete_attribute('already-generated')
2202 propagate_children = proc { |xmldir|
2203 if xmldir.attributes['subdirs-caption']
2204 xmldir.delete_attribute('already-generated')
2206 xmldir.elements.each('dir') { |element|
2207 propagate_children.call(element)
2211 if $xmldir.child_byname_notattr('dir', 'deleted')
2212 new_title = $subalbums_title.buffer.text
2213 if new_title != $xmldir.attributes['subdirs-caption']
2214 parent = $xmldir.parent
2215 if parent.name == 'dir'
2216 parent.delete_attribute('already-generated')
2218 propagate_children.call($xmldir)
2220 $xmldir.add_attribute('subdirs-caption', new_title)
2221 $xmldir.elements.each('dir') { |element|
2222 if !element.attributes['deleted']
2223 path = element.attributes['path']
2224 newtext = $subalbums_edits[path][:editzone].buffer.text
2225 if element.attributes['subdirs-caption']
2226 if element.attributes['subdirs-caption'] != newtext
2227 propagate_children.call(element)
2229 element.add_attribute('subdirs-caption', newtext)
2230 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2232 if element.attributes['thumbnails-caption'] != newtext
2233 element.delete_attribute('already-generated')
2235 element.add_attribute('thumbnails-caption', newtext)
2236 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2242 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2243 if $xmldir.attributes['thumbnails-caption']
2244 path = $xmldir.attributes['path']
2245 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2247 elsif $xmldir.attributes['thumbnails-caption']
2248 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2251 if $xmldir.attributes['thumbnails-caption']
2252 if edit = $subalbums_edits[$xmldir.attributes['path']]
2253 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2257 #- remove and reinsert elements to reflect new ordering
2260 $xmldir.elements.each { |element|
2261 if element.name == 'image' || element.name == 'video'
2262 saves[element.attributes['filename']] = element.remove
2266 $autotable.current_order.each { |path|
2267 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2268 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2271 saves.each_key { |path|
2272 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2273 chld.add_attribute('deleted', 'true')
2277 def sort_by_exif_date
2281 rexml_thread_protect {
2282 $xmldir.elements.each { |element|
2283 if element.name == 'image' || element.name == 'video'
2284 current_order << element.attributes['filename']
2289 #- look for EXIF dates
2292 if current_order.size > 20
2294 w.set_transient_for($main_window)
2296 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2297 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2298 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2299 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2300 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2301 vb.pack_end(bottom, false, false)
2303 w.signal_connect('delete-event') { w.destroy }
2304 w.window_position = Gtk::Window::POS_CENTER
2308 b.signal_connect('clicked') { aborted = true }
2310 current_order.each { |f|
2312 if entry2type(f) == 'image'
2314 pb.fraction = i.to_f / current_order.size
2315 Gtk.main_iteration while Gtk.events_pending?
2316 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2318 dates[f] = date_time
2331 current_order.each { |f|
2332 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2334 dates[f] = date_time
2340 rexml_thread_protect {
2341 $xmldir.elements.each { |element|
2342 if element.name == 'image' || element.name == 'video'
2343 saves[element.attributes['filename']] = element.remove
2348 neworder = smartsort(current_order, dates)
2350 rexml_thread_protect {
2352 $xmldir.add_element(saves[f].name, saves[f].attributes)
2356 #- let the auto-table reflect new ordering
2360 def remove_all_captions
2363 $autotable.current_order.each { |path|
2364 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2365 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2367 save_undo(_("remove all captions"),
2369 texts.each_key { |key|
2370 $name2widgets[key][:textview].buffer.text = texts[key]
2372 $notebook.set_page(1)
2374 texts.each_key { |key|
2375 $name2widgets[key][:textview].buffer.text = ''
2377 $notebook.set_page(1)
2383 $selected_elements.each_key { |path|
2384 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2390 $selected_elements = {}
2394 $undo_tb.sensitive = $undo_mb.sensitive = false
2395 $redo_tb.sensitive = $redo_mb.sensitive = false
2401 $subalbums_vb.children.each { |chld|
2402 $subalbums_vb.remove(chld)
2404 $subalbums = Gtk::Table.new(0, 0, true)
2405 current_y_sub_albums = 0
2407 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2408 $subalbums_edits = {}
2409 subalbums_counter = 0
2410 subalbums_edits_bypos = {}
2412 add_subalbum = proc { |xmldir, counter|
2413 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2414 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2415 if xmldir == $xmldir
2416 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2417 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2418 caption = xmldir.attributes['thumbnails-caption']
2419 infotype = 'thumbnails'
2421 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2422 captionfile, caption = find_subalbum_caption_info(xmldir)
2423 infotype = find_subalbum_info_type(xmldir)
2425 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2426 hbox = Gtk::HBox.new
2427 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2429 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2432 my_gen_real_thumbnail = proc {
2433 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2436 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2437 f.add(img = Gtk::Image.new)
2438 my_gen_real_thumbnail.call
2440 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2442 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2443 $subalbums.attach(hbox,
2444 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2446 frame, textview = create_editzone($subalbums_sw, 0, img)
2447 textview.buffer.text = caption
2448 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2449 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2451 change_image = proc {
2452 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2454 Gtk::FileChooser::ACTION_OPEN,
2456 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2457 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2458 fc.transient_for = $main_window
2459 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))
2460 f.add(preview_img = Gtk::Image.new)
2462 fc.signal_connect('update-preview') { |w|
2463 if fc.preview_filename
2464 if entry2type(fc.preview_filename) == 'video'
2468 tmpdir = gen_video_thumbnail(fc.preview_filename, false, 0)
2470 fc.preview_widget_active = false
2472 tmpimage = "#{tmpdir}/00000001.jpg"
2474 preview_img.pixbuf = Gdk::Pixbuf.new(tmpimage, 240, 180)
2475 fc.preview_widget_active = true
2476 rescue Gdk::PixbufError
2477 fc.preview_widget_active = false
2479 File.delete(tmpimage)
2486 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2487 fc.preview_widget_active = true
2488 rescue Gdk::PixbufError
2489 fc.preview_widget_active = false
2494 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2496 old_file = captionfile
2497 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2498 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2499 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2500 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2502 new_file = fc.filename
2503 msg 3, "new captionfile is: #{fc.filename}"
2504 perform_changefile = proc {
2505 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2506 $modified_pixbufs.delete(thumbnail_file)
2507 xmldir.delete_attribute("#{infotype}-rotate")
2508 xmldir.delete_attribute("#{infotype}-color-swap")
2509 xmldir.delete_attribute("#{infotype}-enhance")
2510 xmldir.delete_attribute("#{infotype}-seektime")
2511 my_gen_real_thumbnail.call
2513 perform_changefile.call
2515 save_undo(_("change caption file for sub-album"),
2517 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2518 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2519 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2520 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2521 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2522 my_gen_real_thumbnail.call
2523 $notebook.set_page(0)
2525 perform_changefile.call
2526 $notebook.set_page(0)
2534 if File.exists?(thumbnail_file)
2535 File.delete(thumbnail_file)
2537 my_gen_real_thumbnail.call
2540 rotate_and_cleanup = proc { |angle|
2541 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2542 if File.exists?(thumbnail_file)
2543 File.delete(thumbnail_file)
2547 move = proc { |direction|
2550 save_changes('forced')
2551 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2552 if direction == 'up'
2553 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2554 subalbums_edits_bypos[oldpos - 1][:position] += 1
2556 if direction == 'down'
2557 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2558 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2560 if direction == 'top'
2561 for i in 1 .. oldpos - 1
2562 subalbums_edits_bypos[i][:position] += 1
2564 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2566 if direction == 'bottom'
2567 for i in oldpos + 1 .. subalbums_counter
2568 subalbums_edits_bypos[i][:position] -= 1
2570 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2574 $xmldir.elements.each('dir') { |element|
2575 if (!element.attributes['deleted'])
2576 elems << [ element.attributes['path'], element.remove ]
2579 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2580 each { |e| $xmldir.add_element(e[1]) }
2581 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2582 $xmldir.elements.each('descendant::dir') { |elem|
2583 elem.delete_attribute('already-generated')
2586 sel = $albums_tv.selection.selected_rows
2588 populate_subalbums_treeview(false)
2589 $albums_tv.selection.select_path(sel[0])
2592 color_swap_and_cleanup = proc {
2593 perform_color_swap_and_cleanup = proc {
2594 color_swap(xmldir, "#{infotype}-")
2595 my_gen_real_thumbnail.call
2597 perform_color_swap_and_cleanup.call
2599 save_undo(_("color swap"),
2601 perform_color_swap_and_cleanup.call
2602 $notebook.set_page(0)
2604 perform_color_swap_and_cleanup.call
2605 $notebook.set_page(0)
2610 change_seektime_and_cleanup = proc {
2611 if values = ask_new_seektime(xmldir, "#{infotype}-")
2612 perform_change_seektime_and_cleanup = proc { |val|
2613 change_seektime(xmldir, "#{infotype}-", val)
2614 my_gen_real_thumbnail.call
2616 perform_change_seektime_and_cleanup.call(values[:new])
2618 save_undo(_("specify seektime"),
2620 perform_change_seektime_and_cleanup.call(values[:old])
2621 $notebook.set_page(0)
2623 perform_change_seektime_and_cleanup.call(values[:new])
2624 $notebook.set_page(0)
2630 whitebalance_and_cleanup = proc {
2631 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2632 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2633 perform_change_whitebalance_and_cleanup = proc { |val|
2634 change_whitebalance(xmldir, "#{infotype}-", val)
2635 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2636 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2637 if File.exists?(thumbnail_file)
2638 File.delete(thumbnail_file)
2641 perform_change_whitebalance_and_cleanup.call(values[:new])
2643 save_undo(_("fix white balance"),
2645 perform_change_whitebalance_and_cleanup.call(values[:old])
2646 $notebook.set_page(0)
2648 perform_change_whitebalance_and_cleanup.call(values[:new])
2649 $notebook.set_page(0)
2655 gammacorrect_and_cleanup = proc {
2656 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2657 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2658 perform_change_gammacorrect_and_cleanup = proc { |val|
2659 change_gammacorrect(xmldir, "#{infotype}-", val)
2660 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2661 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2662 if File.exists?(thumbnail_file)
2663 File.delete(thumbnail_file)
2666 perform_change_gammacorrect_and_cleanup.call(values[:new])
2668 save_undo(_("gamma correction"),
2670 perform_change_gammacorrect_and_cleanup.call(values[:old])
2671 $notebook.set_page(0)
2673 perform_change_gammacorrect_and_cleanup.call(values[:new])
2674 $notebook.set_page(0)
2680 enhance_and_cleanup = proc {
2681 perform_enhance_and_cleanup = proc {
2682 enhance(xmldir, "#{infotype}-")
2683 my_gen_real_thumbnail.call
2686 perform_enhance_and_cleanup.call
2688 save_undo(_("enhance"),
2690 perform_enhance_and_cleanup.call
2691 $notebook.set_page(0)
2693 perform_enhance_and_cleanup.call
2694 $notebook.set_page(0)
2699 evtbox.signal_connect('button-press-event') { |w, event|
2700 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2702 rotate_and_cleanup.call(90)
2704 rotate_and_cleanup.call(-90)
2705 elsif $enhance.active?
2706 enhance_and_cleanup.call
2709 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2710 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2711 { :forbid_left => true, :forbid_right => true,
2712 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2713 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2714 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2715 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2716 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2718 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2723 evtbox.signal_connect('button-press-event') { |w, event|
2724 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2728 evtbox.signal_connect('button-release-event') { |w, event|
2729 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2730 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2731 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2732 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2733 msg 3, "gesture rotate: #{angle}"
2734 rotate_and_cleanup.call(angle)
2737 $gesture_press = nil
2740 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2741 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2742 current_y_sub_albums += 1
2745 if $xmldir.child_byname_notattr('dir', 'deleted')
2747 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2748 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] || ''
2749 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2750 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2751 #- this album image/caption
2752 if $xmldir.attributes['thumbnails-caption']
2753 add_subalbum.call($xmldir, 0)
2756 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2757 $xmldir.elements.each { |element|
2758 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2759 #- element (image or video) of this album
2760 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2761 msg 3, "dest_img: #{dest_img}"
2762 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2763 total[element.name] += 1
2765 if element.name == 'dir' && !element.attributes['deleted']
2766 #- sub-album image/caption
2767 add_subalbum.call(element, subalbums_counter += 1)
2768 total[element.name] += 1
2771 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2772 total['image'], total['video'], total['dir'] ]))
2773 $subalbums_vb.add($subalbums)
2774 $subalbums_vb.show_all
2776 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2777 $notebook.get_tab_label($autotable_sw).sensitive = false
2778 $notebook.set_page(0)
2779 $thumbnails_title.buffer.text = ''
2781 $notebook.get_tab_label($autotable_sw).sensitive = true
2782 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2785 if !$xmldir.child_byname_notattr('dir', 'deleted')
2786 $notebook.get_tab_label($subalbums_sw).sensitive = false
2787 $notebook.set_page(1)
2789 $notebook.get_tab_label($subalbums_sw).sensitive = true
2793 def pixbuf_or_nil(filename)
2795 return Gdk::Pixbuf.new(filename)
2801 def theme_choose(current)
2802 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2804 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2805 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2806 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2808 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2809 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2810 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2811 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2812 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2813 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2814 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2815 treeview.signal_connect('button-press-event') { |w, event|
2816 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2817 dialog.response(Gtk::Dialog::RESPONSE_OK)
2821 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2823 ([ $FPATH + '/themes/simple' ] + (`find '#{$FPATH}/themes' ~/.booh-themes -mindepth 1 -maxdepth 1 -type d 2>/dev/null`.find_all { |e| e !~ /simple$/ }.sort)).each { |dir|
2826 iter[0] = File.basename(dir)
2827 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2828 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2829 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2830 if File.basename(dir) == current
2831 treeview.selection.select_iter(iter)
2834 dialog.set_default_size(-1, 500)
2835 dialog.vbox.show_all
2837 dialog.run { |response|
2838 iter = treeview.selection.selected
2840 if response == Gtk::Dialog::RESPONSE_OK && iter
2841 return model.get_value(iter, 0)
2847 def show_password_protections
2848 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2849 child_iter = $albums_iters[xmldir.attributes['path']]
2850 if xmldir.attributes['password-protect']
2851 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2852 already_protected = true
2853 elsif already_protected
2854 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2856 pix = pix.saturate_and_pixelate(1, true)
2862 xmldir.elements.each('dir') { |elem|
2863 if !elem.attributes['deleted']
2864 examine_dir_elem.call(child_iter, elem, already_protected)
2868 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2871 def populate_subalbums_treeview(select_first)
2875 $subalbums_vb.children.each { |chld|
2876 $subalbums_vb.remove(chld)
2879 source = $xmldoc.root.attributes['source']
2880 msg 3, "source: #{source}"
2882 xmldir = $xmldoc.elements['//dir']
2883 if !xmldir || xmldir.attributes['path'] != source
2884 msg 1, _("Corrupted booh file...")
2888 append_dir_elem = proc { |parent_iter, xmldir|
2889 child_iter = $albums_ts.append(parent_iter)
2890 child_iter[0] = File.basename(xmldir.attributes['path'])
2891 child_iter[1] = xmldir.attributes['path']
2892 $albums_iters[xmldir.attributes['path']] = child_iter
2893 msg 3, "puttin location: #{xmldir.attributes['path']}"
2894 xmldir.elements.each('dir') { |elem|
2895 if !elem.attributes['deleted']
2896 append_dir_elem.call(child_iter, elem)
2900 append_dir_elem.call(nil, xmldir)
2901 show_password_protections
2903 $albums_tv.expand_all
2905 $albums_tv.selection.select_iter($albums_ts.iter_first)
2909 def select_current_theme
2910 select_theme($xmldoc.root.attributes['theme'],
2911 $xmldoc.root.attributes['limit-sizes'],
2912 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2913 $xmldoc.root.attributes['thumbnails-per-row'])
2916 def open_file(filename)
2920 $current_path = nil #- invalidate
2921 $modified_pixbufs = {}
2924 $subalbums_vb.children.each { |chld|
2925 $subalbums_vb.remove(chld)
2928 if !File.exists?(filename)
2929 return utf8(_("File not found."))
2933 $xmldoc = REXML::Document.new(File.new(filename))
2938 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2939 if entry2type(filename).nil?
2940 return utf8(_("Not a booh file!"))
2942 return utf8(_("Not a booh file!\n\nHint: you cannot import directly a photo or video with File/Open.\nUse File/New to create a new album."))
2946 if !source = $xmldoc.root.attributes['source']
2947 return utf8(_("Corrupted booh file..."))
2950 if !dest = $xmldoc.root.attributes['destination']
2951 return utf8(_("Corrupted booh file..."))
2954 if !theme = $xmldoc.root.attributes['theme']
2955 return utf8(_("Corrupted booh file..."))
2958 if $xmldoc.root.attributes['version'] < '0.9.0'
2959 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2960 mark_document_as_dirty
2961 if $xmldoc.root.attributes['version'] < '0.8.4'
2962 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2963 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2964 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2965 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2966 if old_dest_dir != new_dest_dir
2967 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2969 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2970 xmldir.elements.each { |element|
2971 if %w(image video).include?(element.name) && !element.attributes['deleted']
2972 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2973 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2974 Dir[old_name + '*'].each { |file|
2975 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2976 file != new_file and sys("mv '#{file}' '#{new_file}'")
2979 if element.name == 'dir' && !element.attributes['deleted']
2980 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2981 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2982 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2986 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2990 $xmldoc.root.add_attribute('version', $VERSION)
2993 select_current_theme
2995 $filename = filename
2996 set_mainwindow_title(nil)
2997 $default_size['thumbnails'] =~ /(.*)x(.*)/
2998 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2999 $albums_thumbnail_size =~ /(.*)x(.*)/
3000 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3002 populate_subalbums_treeview(true)
3004 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $extend.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
3008 def open_file_user(filename)
3009 result = open_file(filename)
3011 $config['last-opens'] ||= []
3012 if $config['last-opens'][-1] != utf8(filename)
3013 $config['last-opens'] << utf8(filename)
3015 $orig_filename = $filename
3016 $main_window.title = 'booh - ' + File.basename($orig_filename)
3017 tmp = Tempfile.new("boohtemp")
3018 Thread.critical = true
3019 $filename = tmp.path
3022 ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
3023 Thread.critical = false
3025 $tempfiles << $filename << "#{$filename}.backup"
3027 $orig_filename = nil
3033 if !ask_save_modifications(utf8(_("Save this album?")),
3034 utf8(_("Do you want to save the changes to this album?")),
3035 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3038 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
3040 Gtk::FileChooser::ACTION_OPEN,
3042 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3043 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3044 fc.set_current_folder(File.expand_path("~/.booh"))
3045 fc.transient_for = $main_window
3046 fc.preview_widget = previewlabel = Gtk::Label.new.show
3047 fc.signal_connect('update-preview') { |w|
3048 if fc.preview_filename
3050 push_mousecursor_wait(fc)
3051 xmldoc = REXML::Document.new(File.new(fc.preview_filename))
3055 xmldoc.elements.each('//*') { |elem|
3056 if elem.name == 'dir'
3058 elsif elem.name == 'image'
3060 elsif elem.name == 'video'
3068 if !xmldoc || !xmldoc.root || xmldoc.root.name != 'booh'
3069 fc.preview_widget_active = false
3071 previewlabel.markup = utf8(_("<i>Source:</i> %s\n<i>Destination:</i> %s\n<i>Subalbums:</i> %s\n<i>Images:</i> %s\n<i>Videos:</i> %s") %
3072 [ xmldoc.root.attributes['source'], xmldoc.root.attributes['destination'], subalbums, images, videos ])
3073 fc.preview_widget_active = true
3079 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3080 push_mousecursor_wait(fc)
3081 msg = open_file_user(fc.filename)
3096 def additional_booh_options
3099 options += "--mproc #{$config['mproc'].to_i} "
3101 options += "--comments-format '#{$config['comments-format']}' "
3102 if $config['transcode-videos']
3103 options += "--transcode-videos '#{$config['transcode-videos']}' "
3108 def ask_multi_languages(value)
3110 spl = value.split(',')
3111 value = [ spl[0..-2], spl[-1] ]
3114 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
3117 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3118 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3120 lbl = Gtk::Label.new
3122 _("You can choose to activate <b>multi-languages</b> support for this web-album
3123 (it will work only if you publish your web-album on an Apache web-server). This will
3124 use the MultiViews feature of Apache; the pages will be served according to the
3125 value of the Accept-Language HTTP header sent by the web browsers, so that people
3126 with different languages preferences will be able to browse your web-album with
3127 navigation in their language (if language is available).
3130 dialog.vbox.add(lbl)
3131 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3132 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3133 add(languages = Gtk::Button.new))))
3135 pick_languages = proc {
3136 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3139 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3140 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3142 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3143 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3145 SUPPORTED_LANGUAGES.each { |lang|
3146 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3147 if ! value.nil? && value[0].include?(lang)
3153 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3154 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3155 fallback_language = nil
3156 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3157 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3158 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3159 fbl_rb.active = true
3160 fallback_language = SUPPORTED_LANGUAGES[0]
3162 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3163 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3164 rb.signal_connect('clicked') { fallback_language = lang }
3165 if ! value.nil? && value[1] == lang
3170 dialog2.window_position = Gtk::Window::POS_MOUSE
3174 dialog2.run { |response|
3176 if resp == Gtk::Dialog::RESPONSE_OK
3178 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3179 value[1] = fallback_language
3180 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3187 languages.signal_connect('clicked') {
3190 dialog.window_position = Gtk::Window::POS_MOUSE
3194 rb_yes.active = true
3195 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3197 rb_no.signal_connect('clicked') {
3201 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3214 dialog.run { |response|
3219 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3221 return [ true, nil ]
3223 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3232 if !ask_save_modifications(utf8(_("Save this album?")),
3233 utf8(_("Do you want to save the changes to this album?")),
3234 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3237 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3239 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3240 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3241 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3243 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3244 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3245 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3246 tbl.attach(src = Gtk::Entry.new,
3247 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3248 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3249 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3250 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3251 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3252 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3253 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3254 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3255 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3256 tbl.attach(dest = Gtk::Entry.new,
3257 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3258 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3259 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3260 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3261 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3262 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3263 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3264 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3265 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3267 tooltips = Gtk::Tooltips.new
3268 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3269 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3270 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3271 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3272 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3273 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3274 tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of photos from point-and-shoot cameras - also called compact cameras - is 4/3, whereas photos from SLR cameras - also called reflex cameras - is 3/2)")), nil)
3275 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3276 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3277 nperpage_model = Gtk::ListStore.new(String, String)
3278 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3279 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3280 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3281 nperpagecombo.set_attributes(crt, { :markup => 0 })
3282 iter = nperpage_model.append
3283 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3285 [ 12, 20, 30, 40, 50 ].each { |v|
3286 iter = nperpage_model.append
3287 iter[0] = iter[1] = v.to_s
3289 nperpagecombo.active = 0
3291 multilanguages_value = nil
3292 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3293 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3294 tooltips.set_tip(ml, utf8(_("When disabled, the web-album will be generated with navigation in your desktop language. When enabled, the web-album will be generated with navigation in all languages you select, but you have to publish your web-album on an Apache web-server for that feature to work.")), nil)
3295 multilanguages.signal_connect('clicked') {
3296 retval = ask_multi_languages(multilanguages_value)
3298 multilanguages_value = retval[1]
3300 if multilanguages_value
3301 ml_label.text = utf8(_("Multi-languages: enabled."))
3303 ml_label.text = utf8(_("Multi-languages: disabled."))
3306 if $config['default-multi-languages']
3307 multilanguages_value = $config['default-multi-languages']
3308 ml_label.text = utf8(_("Multi-languages: enabled."))
3310 ml_label.text = utf8(_("Multi-languages: disabled."))
3313 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3314 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3315 tooltips.set_tip(indexlinkentry, utf8(_("Optional HTML markup to use on pages bottom for a small link returning to wherever you see fit in your website (or somewhere else)")), nil)
3316 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3317 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3318 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)
3320 src_nb_calculated_for = ''
3322 process_src_nb = proc {
3323 if src.text != src_nb_calculated_for
3324 src_nb_calculated_for = src.text
3326 Thread.kill(src_nb_thread)
3329 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3330 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3332 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3333 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3334 src_nb_thread = Thread.new {
3335 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3336 total = { 'image' => 0, 'video' => 0, nil => 0 }
3337 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3338 if File.basename(dir) =~ /^\./
3342 Dir.entries(dir.chomp).each { |file|
3343 total[entry2type(file)] += 1
3345 rescue Errno::EACCES, Errno::ENOENT
3349 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3353 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3356 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3362 timeout_src_nb = Gtk.timeout_add(100) {
3366 src_browse.signal_connect('clicked') {
3367 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3369 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3371 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3372 fc.transient_for = $main_window
3373 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3374 src.text = utf8(fc.filename)
3376 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3381 dest_browse.signal_connect('clicked') {
3382 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3384 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3386 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3387 fc.transient_for = $main_window
3388 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3389 dest.text = utf8(fc.filename)
3394 conf_browse.signal_connect('clicked') {
3395 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3397 Gtk::FileChooser::ACTION_SAVE,
3399 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3400 fc.transient_for = $main_window
3401 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3402 fc.set_current_folder(File.expand_path("~/.booh"))
3403 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3404 conf.text = utf8(fc.filename)
3411 recreate_theme_config = proc {
3412 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3414 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3415 $images_size.each { |s|
3416 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3420 tooltips.set_tip(cb, utf8(s['description']), nil)
3421 theme_sizes << { :widget => cb, :value => s['name'] }
3423 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3424 tooltips = Gtk::Tooltips.new
3425 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3426 theme_sizes << { :widget => cb, :value => 'original' }
3429 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3432 $allowed_N_values.each { |n|
3434 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3436 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3438 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3442 nperrows << { :widget => rb, :value => n }
3444 nperrowradios.show_all
3446 recreate_theme_config.call
3448 theme_button.signal_connect('clicked') {
3449 if newtheme = theme_choose(theme_button.label)
3450 theme_button.label = newtheme
3451 recreate_theme_config.call
3455 dialog.vbox.add(frame1)
3456 dialog.vbox.add(frame2)
3462 dialog.run { |response|
3463 if response == Gtk::Dialog::RESPONSE_OK
3464 srcdir = from_utf8_safe(src.text)
3465 destdir = from_utf8_safe(dest.text)
3466 confpath = from_utf8_safe(conf.text)
3467 if src.text != '' && srcdir == ''
3468 show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3470 elsif !File.directory?(srcdir)
3471 show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3473 elsif dest.text != '' && destdir == ''
3474 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3476 elsif destdir != make_dest_filename(destdir)
3477 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3479 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3480 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3481 inside it will be permanently removed before creating the web-album!
3482 Are you sure you want to continue?")), { :okcancel => true })
3484 elsif File.exists?(destdir) && !File.directory?(destdir)
3485 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3487 elsif conf.text == ''
3488 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3490 elsif conf.text != '' && confpath == ''
3491 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3493 elsif File.directory?(confpath)
3494 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3496 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3497 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3499 system("mkdir '#{destdir}'")
3500 if !File.directory?(destdir)
3501 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3513 srcdir = from_utf8(src.text)
3514 destdir = from_utf8(dest.text)
3515 configskel = File.expand_path(from_utf8(conf.text))
3516 theme = theme_button.label
3517 #- some sort of automatic theme preference
3518 $config['default-theme'] = theme
3519 $config['default-multi-languages'] = multilanguages_value
3520 $config['default-optimize32'] = optimize432.active?.to_s
3521 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3522 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3523 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3524 opt432 = optimize432.active?
3525 madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3526 indexlink = indexlinkentry.text.gsub('\'', ''')
3529 Thread.kill(src_nb_thread)
3530 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3533 Gtk.timeout_remove(timeout_src_nb)
3536 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3537 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3538 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3539 (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
3540 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3541 utf8(_("Please wait while scanning source directory...")),
3543 { :closure_after => proc {
3544 open_file_user(configskel)
3545 $main_window.urgency_hint = true
3551 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3553 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3554 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3555 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3557 source = $xmldoc.root.attributes['source']
3558 dest = $xmldoc.root.attributes['destination']
3559 theme = $xmldoc.root.attributes['theme']
3560 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3561 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3562 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3563 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3565 limit_sizes = limit_sizes.split(/,/)
3567 madewith = ($xmldoc.root.attributes['made-with'] || '').gsub(''', '\'')
3568 indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub(''', '\'')
3569 save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3571 tooltips = Gtk::Tooltips.new
3572 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3573 tbl.attach(Gtk::Label.new(utf8(_("Directory of source photos/videos: "))),
3574 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3575 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3576 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3577 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3578 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3579 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3580 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3581 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3582 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3583 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3584 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3586 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3587 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3588 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3589 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3590 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3591 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3592 tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of photos from point-and-shoot cameras - also called compact cameras - is 4/3, whereas photos from SLR cameras - also called reflex cameras - is 3/2)")), nil)
3593 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3594 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3595 nperpage_model = Gtk::ListStore.new(String, String)
3596 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3597 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3598 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3599 nperpagecombo.set_attributes(crt, { :markup => 0 })
3600 iter = nperpage_model.append
3601 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3603 [ 12, 20, 30, 40, 50 ].each { |v|
3604 iter = nperpage_model.append
3605 iter[0] = iter[1] = v.to_s
3606 if nperpage && nperpage == v.to_s
3607 nperpagecombo.active_iter = iter
3610 if nperpagecombo.active_iter.nil?
3611 nperpagecombo.active = 0
3614 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3615 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3616 tooltips.set_tip(ml, utf8(_("When disabled, the web-album will be generated with navigation in your desktop language. When enabled, the web-album will be generated with navigation in all languages you select, but you have to publish your web-album on an Apache web-server for that feature to work.")), nil)
3618 if save_multilanguages_value
3619 ml_label.text = utf8(_("Multi-languages: enabled."))
3621 ml_label.text = utf8(_("Multi-languages: disabled."))
3625 multilanguages.signal_connect('clicked') {
3626 retval = ask_multi_languages(save_multilanguages_value)
3628 save_multilanguages_value = retval[1]
3633 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3634 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3636 indexlinkentry.text = indexlink
3638 tooltips.set_tip(indexlinkentry, utf8(_("Optional HTML markup to use on pages bottom for a small link returning to wherever you see fit in your website (or somewhere else)")), nil)
3639 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3640 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3642 madewithentry.text = madewith
3644 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)
3648 recreate_theme_config = proc {
3649 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3651 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3653 $images_size.each { |s|
3654 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3656 if limit_sizes.include?(s['name'])
3664 tooltips.set_tip(cb, utf8(s['description']), nil)
3665 theme_sizes << { :widget => cb, :value => s['name'] }
3667 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3668 tooltips = Gtk::Tooltips.new
3669 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3670 if limit_sizes && limit_sizes.include?('original')
3673 theme_sizes << { :widget => cb, :value => 'original' }
3676 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3679 $allowed_N_values.each { |n|
3681 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3683 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3685 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3686 nperrowradios.add(Gtk::Label.new(' '))
3687 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3690 nperrows << { :widget => rb, :value => n.to_s }
3692 nperrowradios.show_all
3694 recreate_theme_config.call
3696 theme_button.signal_connect('clicked') {
3697 if newtheme = theme_choose(theme_button.label)
3700 theme_button.label = newtheme
3701 recreate_theme_config.call
3705 dialog.vbox.add(frame1)
3706 dialog.vbox.add(frame2)
3712 dialog.run { |response|
3713 if response == Gtk::Dialog::RESPONSE_OK
3714 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3715 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3724 save_theme = theme_button.label
3725 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3726 save_opt432 = optimize432.active?
3727 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3728 save_nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3729 save_madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3730 save_indexlink = indexlinkentry.text.gsub('\'', ''')
3733 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_nperpage != nperpage || save_madewith != madewith || save_indexlink != indexlinkentry || save_multilanguages_value != multilanguages_value)
3734 #- some sort of automatic preferences
3735 if save_theme != theme
3736 $config['default-theme'] = save_theme
3738 if save_multilanguages_value != multilanguages_value
3739 $config['default-multi-languages'] = save_multilanguages_value
3741 if save_opt432 != opt432
3742 $config['default-optimize32'] = save_opt432.to_s
3744 mark_document_as_dirty
3746 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3747 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3748 (save_nperpage ? "--thumbnails-per-page #{save_nperpage} " : '') +
3749 (save_multilanguages_value ? "--multi-languages #{save_multilanguages_value} " : '') +
3750 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3751 utf8(_("Please wait while scanning source directory...")),
3753 { :closure_after => proc {
3754 open_file($filename)
3756 $main_window.urgency_hint = true
3759 #- select_theme merges global variables, need to return to current choices
3760 select_current_theme
3767 sel = $albums_tv.selection.selected_rows
3769 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3770 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3771 utf8(_("Please wait while scanning source directory...")),
3773 { :closure_after => proc {
3774 open_file($filename)
3775 $albums_tv.selection.select_path(sel[0])
3777 $main_window.urgency_hint = true
3784 sel = $albums_tv.selection.selected_rows
3786 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3787 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3788 utf8(_("Please wait while scanning source directory...")),
3790 { :closure_after => proc {
3791 open_file($filename)
3792 $albums_tv.selection.select_path(sel[0])
3794 $main_window.urgency_hint = true
3801 theme = $xmldoc.root.attributes['theme']
3802 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3804 limit_sizes = "--sizes #{limit_sizes}"
3806 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3807 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3808 utf8(_("Please wait while scanning source directory...")),
3810 { :closure_after => proc {
3811 open_file($filename)
3813 $main_window.urgency_hint = true
3818 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3820 Gtk::FileChooser::ACTION_SAVE,
3822 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3823 fc.transient_for = $main_window
3824 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3825 fc.set_current_folder(File.expand_path("~/.booh"))
3826 fc.filename = $orig_filename
3827 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3828 $orig_filename = fc.filename
3829 if ! save_current_file_user
3833 $config['last-opens'] ||= []
3834 $config['last-opens'] << $orig_filename
3840 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3842 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3843 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3844 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3846 dialog.vbox.add(notebook = Gtk::Notebook.new)
3847 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3848 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3849 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3850 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)),
3851 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3852 tooltips = Gtk::Tooltips.new
3853 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3854 for example: /usr/bin/mplayer %f")), nil)
3855 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3856 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3857 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3858 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3859 tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3860 for example: /usr/bin/gimp-remote %f")), nil)
3861 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3862 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3863 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3864 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3865 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3866 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3867 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3868 0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3869 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)),
3870 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3871 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)
3872 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3873 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3874 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)
3875 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original photos/videos as well"))),
3876 0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3877 tooltips.set_tip(deleteondisk_check, utf8(_("Normally, deleting a photo 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)
3879 smp_check.signal_connect('toggled') {
3880 smp_hbox.sensitive = smp_check.active?
3883 smp_check.active = true
3884 smp_spin.value = $config['mproc'].to_i
3886 nogestures_check.active = $config['nogestures']
3887 deleteondisk_check.active = $config['deleteondisk']
3889 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3890 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3891 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3892 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3893 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3894 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nphotos in new albums:"))),
3895 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3896 tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3897 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3898 tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3899 2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3900 tooltips.set_tip(commentsformat_entry, utf8(_("Normally, filenames without extension are used as comments for photos and videos in new albums. Use this entry to use something else.")), nil)
3901 commentsformat_help.signal_connect('clicked') {
3902 show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3903 hence you should look at ImageMagick/identify documentation for the most
3904 accurate and up-to-date documentation. Last time I checked, documentation
3907 Print information about the image in a format of your choosing. You can
3908 include the image filename, type, width, height, Exif data, or other image
3909 attributes by embedding special format characters:
3912 %P page width and height
3916 %e filename extension
3921 %k number of unique colors
3928 %r image class and colorspace
3931 %u unique temporary filename
3944 displays MIFF:bird.miff 512x480 for an image titled bird.miff and whose
3945 width is 512 and height is 480.
3947 If the first character of string is @, the format is read from a file titled
3948 by the remaining characters in the string.
3950 You can also use the following special formatting syntax to print Exif
3951 information contained in the file:
3955 Where tag can be one of the following:
3957 * (print all Exif tags, in keyword=data format)
3958 ! (print all Exif tags, in tag_number data format)
3959 #hhhh (print data for Exif tag #hhhh)
3964 PhotometricInterpretation
3984 PrimaryChromaticities
3987 JPEGInterchangeFormat
3988 JPEGInterchangeFormatLength