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 rexml_thread_protect {
549 value = 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 rexml_thread_protect {
610 value = 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
886 $gen_thumbnail_monitor.synchronize {
887 $current_gen_thumbnail_threads -= 1
895 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
896 distribute_multiple_call = Proc.new { |action, arg|
897 $selected_elements.each_key { |path|
898 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
900 if possible_actions[:can_multiple] && $selected_elements.length > 0
901 UndoHandler.begin_batch
902 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
903 UndoHandler.end_batch
905 closures[action].call(arg)
907 $selected_elements = {}
910 if optionals.include?('change_image')
911 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
912 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
913 changeimg.signal_connect('activate') { closures[:change].call }
914 menu.append(Gtk::SeparatorMenuItem.new)
916 if !possible_actions[:can_multiple] || $selected_elements.length == 0
919 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
920 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
921 view.signal_connect('activate') { closures[:view].call }
923 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
924 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
925 view.signal_connect('activate') { closures[:view].call }
926 menu.append(Gtk::SeparatorMenuItem.new)
929 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
930 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
931 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
932 exif.signal_connect('activate') { show_popup($main_window,
933 utf8(`exif -m '#{fullpath}'`),
934 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
935 menu.append(Gtk::SeparatorMenuItem.new)
938 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
939 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
940 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
941 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
942 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
943 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
944 if !possible_actions[:can_multiple] || $selected_elements.length == 0
945 menu.append(Gtk::SeparatorMenuItem.new)
946 if !possible_actions[:forbid_left]
947 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
948 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
949 moveleft.signal_connect('activate') { closures[:move].call('left') }
950 if !possible_actions[:can_left]
951 moveleft.sensitive = false
954 if !possible_actions[:forbid_right]
955 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
956 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
957 moveright.signal_connect('activate') { closures[:move].call('right') }
958 if !possible_actions[:can_right]
959 moveright.sensitive = false
962 if optionals.include?('move_top')
963 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
964 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
965 movetop.signal_connect('activate') { closures[:move].call('top') }
966 if !possible_actions[:can_top]
967 movetop.sensitive = false
970 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
971 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
972 moveup.signal_connect('activate') { closures[:move].call('up') }
973 if !possible_actions[:can_up]
974 moveup.sensitive = false
976 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
977 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
978 movedown.signal_connect('activate') { closures[:move].call('down') }
979 if !possible_actions[:can_down]
980 movedown.sensitive = false
982 if optionals.include?('move_bottom')
983 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
984 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
985 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
986 if !possible_actions[:can_bottom]
987 movebottom.sensitive = false
992 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
993 menu.append(Gtk::SeparatorMenuItem.new)
994 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
995 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
996 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
997 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
998 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
999 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
1000 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
1001 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
1002 seektime.signal_connect('activate') {
1003 if possible_actions[:can_multiple] && $selected_elements.length > 0
1004 if values = ask_new_seektime(nil, '')
1005 distribute_multiple_call.call(:seektime, values)
1008 closures[:seektime].call
1013 menu.append( Gtk::SeparatorMenuItem.new)
1014 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
1015 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
1016 gammacorrect.signal_connect('activate') {
1017 if possible_actions[:can_multiple] && $selected_elements.length > 0
1018 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
1019 distribute_multiple_call.call(:gammacorrect, values)
1022 closures[:gammacorrect].call
1025 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
1026 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
1027 whitebalance.signal_connect('activate') {
1028 if possible_actions[:can_multiple] && $selected_elements.length > 0
1029 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
1030 distribute_multiple_call.call(:whitebalance, values)
1033 closures[:whitebalance].call
1036 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1037 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(rexml_thread_protect { xmldir.attributes["#{attributes_prefix}enhance"] } ? _("Original contrast") :
1038 _("Enhance constrast"))))
1040 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
1042 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1043 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
1044 if type == 'image' && possible_actions[:can_panorama]
1045 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
1046 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
1047 panorama.signal_connect('activate') {
1048 if possible_actions[:can_multiple] && $selected_elements.length > 0
1049 if values = ask_new_pano_amount(nil, '')
1050 distribute_multiple_call.call(:pano, values)
1053 distribute_multiple_call.call(:pano)
1057 menu.append( Gtk::SeparatorMenuItem.new)
1058 if optionals.include?('delete')
1059 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1060 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1061 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1062 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1063 paste_item.signal_connect('activate') { closures[:paste].call }
1064 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1065 clear_item.signal_connect('activate') { $cuts = [] }
1067 paste_item.sensitive = clear_item.sensitive = false
1070 menu.append( Gtk::SeparatorMenuItem.new)
1072 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1073 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1074 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1075 editexternally.signal_connect('activate') {
1076 if check_image_editor
1077 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1083 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1084 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1085 if optionals.include?('delete')
1086 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1087 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1090 menu.popup(nil, nil, event.button, event.time)
1093 def delete_current_subalbum
1095 sel = $albums_tv.selection.selected_rows
1096 $xmldir.elements.each { |e|
1097 if e.name == 'image' || e.name == 'video'
1098 e.add_attribute('deleted', 'true')
1101 #- branch if we have a non deleted subalbum
1102 if $xmldir.child_byname_notattr('dir', 'deleted')
1103 $xmldir.delete_attribute('thumbnails-caption')
1104 $xmldir.delete_attribute('thumbnails-captionfile')
1106 $xmldir.add_attribute('deleted', 'true')
1108 while moveup.parent.name == 'dir'
1109 moveup = moveup.parent
1110 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1111 moveup.add_attribute('deleted', 'true')
1118 save_changes('forced')
1119 populate_subalbums_treeview(false)
1120 $albums_tv.selection.select_path(sel[0])
1126 $current_path = nil #- prevent save_changes from being rerun again
1127 sel = $albums_tv.selection.selected_rows
1128 restore_one = proc { |xmldir|
1129 xmldir.elements.each { |e|
1130 if e.name == 'dir' && e.attributes['deleted']
1133 e.delete_attribute('deleted')
1136 restore_one.call($xmldir)
1137 populate_subalbums_treeview(false)
1138 $albums_tv.selection.select_path(sel[0])
1141 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1144 frame1 = Gtk::Frame.new
1145 fullpath = from_utf8("#{$current_path}/#{filename}")
1147 my_gen_real_thumbnail = proc {
1148 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1152 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1153 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1154 pack_start(img = Gtk::Image.new).
1155 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1156 px, mask = pxb.render_pixmap_and_mask(0)
1157 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1158 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1160 frame1.add(img = Gtk::Image.new)
1163 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1164 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1165 my_gen_real_thumbnail.call
1167 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1170 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1172 tooltips = Gtk::Tooltips.new
1173 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1174 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1176 frame2, textview = create_editzone($autotable_sw, 1, img)
1177 textview.buffer.text = caption
1178 textview.set_justification(Gtk::Justification::CENTER)
1180 vbox = Gtk::VBox.new(false, 5)
1181 vbox.pack_start(evtbox, false, false)
1182 vbox.pack_start(frame2, false, false)
1183 autotable.append(vbox, filename)
1185 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1186 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1188 #- to be able to find widgets by name
1189 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1191 cleanup_all_thumbnails = proc {
1192 #- remove out of sync images
1193 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1194 for sizeobj in $images_size
1195 #- cannot use sizeobj because panoramic images will have a larger width
1196 Dir.glob("#{dest_img_base}-*.jpg") do |file|
1204 cleanup_all_thumbnails.call
1205 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1207 rexml_thread_protect {
1208 $xmldir.delete_attribute('already-generated')
1210 my_gen_real_thumbnail.call
1213 rotate_and_cleanup = proc { |angle|
1214 cleanup_all_thumbnails.call
1215 rexml_thread_protect {
1216 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1220 move = proc { |direction|
1221 do_method = "move_#{direction}"
1222 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1224 done = autotable.method(do_method).call(vbox)
1225 textview.grab_focus #- because if moving, focus is stolen
1229 save_undo(_("move %s") % direction,
1231 autotable.method(undo_method).call(vbox)
1232 textview.grab_focus #- because if moving, focus is stolen
1233 autoscroll_if_needed($autotable_sw, img, textview)
1234 $notebook.set_page(1)
1236 autotable.method(do_method).call(vbox)
1237 textview.grab_focus #- because if moving, focus is stolen
1238 autoscroll_if_needed($autotable_sw, img, textview)
1239 $notebook.set_page(1)
1245 color_swap_and_cleanup = proc {
1246 perform_color_swap_and_cleanup = proc {
1247 cleanup_all_thumbnails.call
1248 rexml_thread_protect {
1249 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1251 my_gen_real_thumbnail.call
1254 perform_color_swap_and_cleanup.call
1256 save_undo(_("color swap"),
1258 perform_color_swap_and_cleanup.call
1260 autoscroll_if_needed($autotable_sw, img, textview)
1261 $notebook.set_page(1)
1263 perform_color_swap_and_cleanup.call
1265 autoscroll_if_needed($autotable_sw, img, textview)
1266 $notebook.set_page(1)
1271 change_seektime_and_cleanup_real = proc { |values|
1272 perform_change_seektime_and_cleanup = proc { |val|
1273 cleanup_all_thumbnails.call
1274 rexml_thread_protect {
1275 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1277 my_gen_real_thumbnail.call
1279 perform_change_seektime_and_cleanup.call(values[:new])
1281 save_undo(_("specify seektime"),
1283 perform_change_seektime_and_cleanup.call(values[:old])
1285 autoscroll_if_needed($autotable_sw, img, textview)
1286 $notebook.set_page(1)
1288 perform_change_seektime_and_cleanup.call(values[:new])
1290 autoscroll_if_needed($autotable_sw, img, textview)
1291 $notebook.set_page(1)
1296 change_seektime_and_cleanup = proc {
1297 rexml_thread_protect {
1298 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1299 change_seektime_and_cleanup_real.call(values)
1304 change_pano_amount_and_cleanup_real = proc { |values|
1305 perform_change_pano_amount_and_cleanup = proc { |val|
1306 cleanup_all_thumbnails.call
1307 rexml_thread_protect {
1308 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1311 perform_change_pano_amount_and_cleanup.call(values[:new])
1313 save_undo(_("change panorama amount"),
1315 perform_change_pano_amount_and_cleanup.call(values[:old])
1317 autoscroll_if_needed($autotable_sw, img, textview)
1318 $notebook.set_page(1)
1320 perform_change_pano_amount_and_cleanup.call(values[:new])
1322 autoscroll_if_needed($autotable_sw, img, textview)
1323 $notebook.set_page(1)
1328 change_pano_amount_and_cleanup = proc {
1329 rexml_thread_protect {
1330 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1331 change_pano_amount_and_cleanup_real.call(values)
1336 whitebalance_and_cleanup_real = proc { |values|
1337 perform_change_whitebalance_and_cleanup = proc { |val|
1338 cleanup_all_thumbnails.call
1339 rexml_thread_protect {
1340 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1341 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1342 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1345 perform_change_whitebalance_and_cleanup.call(values[:new])
1347 save_undo(_("fix white balance"),
1349 perform_change_whitebalance_and_cleanup.call(values[:old])
1351 autoscroll_if_needed($autotable_sw, img, textview)
1352 $notebook.set_page(1)
1354 perform_change_whitebalance_and_cleanup.call(values[:new])
1356 autoscroll_if_needed($autotable_sw, img, textview)
1357 $notebook.set_page(1)
1362 whitebalance_and_cleanup = proc {
1363 rexml_thread_protect {
1364 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1365 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1366 whitebalance_and_cleanup_real.call(values)
1371 gammacorrect_and_cleanup_real = proc { |values|
1372 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1373 cleanup_all_thumbnails.call
1374 rexml_thread_protect {
1375 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1376 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1377 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1380 perform_change_gammacorrect_and_cleanup.call(values[:new])
1382 save_undo(_("gamma correction"),
1384 perform_change_gammacorrect_and_cleanup.call(values[:old])
1386 autoscroll_if_needed($autotable_sw, img, textview)
1387 $notebook.set_page(1)
1389 perform_change_gammacorrect_and_cleanup.call(values[:new])
1391 autoscroll_if_needed($autotable_sw, img, textview)
1392 $notebook.set_page(1)
1397 gammacorrect_and_cleanup = Proc.new {
1398 rexml_thread_protect {
1399 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1400 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1401 gammacorrect_and_cleanup_real.call(values)
1406 enhance_and_cleanup = proc {
1407 perform_enhance_and_cleanup = proc {
1408 cleanup_all_thumbnails.call
1409 rexml_thread_protect {
1410 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1412 my_gen_real_thumbnail.call
1415 cleanup_all_thumbnails.call
1416 perform_enhance_and_cleanup.call
1418 save_undo(_("enhance"),
1420 perform_enhance_and_cleanup.call
1422 autoscroll_if_needed($autotable_sw, img, textview)
1423 $notebook.set_page(1)
1425 perform_enhance_and_cleanup.call
1427 autoscroll_if_needed($autotable_sw, img, textview)
1428 $notebook.set_page(1)
1433 delete = proc { |isacut|
1434 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 })
1437 perform_delete = proc {
1438 after = autotable.get_next_widget(vbox)
1440 after = autotable.get_previous_widget(vbox)
1442 if $config['deleteondisk'] && !isacut
1443 msg 3, "scheduling for delete: #{fullpath}"
1444 $todelete << fullpath
1446 autotable.remove_widget(vbox)
1448 $vbox2widgets[after][:textview].grab_focus
1449 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1453 previous_pos = autotable.get_current_number(vbox)
1457 delete_current_subalbum
1459 save_undo(_("delete"),
1461 autotable.reinsert(pos, vbox, filename)
1462 $notebook.set_page(1)
1463 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1465 msg 3, "removing deletion schedule of: #{fullpath}"
1466 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1469 $notebook.set_page(1)
1478 $cuts << { :vbox => vbox, :filename => filename }
1479 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1484 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1487 autotable.queue_draws << proc {
1488 $vbox2widgets[last[:vbox]][:textview].grab_focus
1489 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1491 save_undo(_("paste"),
1493 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1494 $notebook.set_page(1)
1497 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1499 $notebook.set_page(1)
1502 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1507 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1508 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1509 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1510 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1512 textview.signal_connect('key-press-event') { |w, event|
1515 x, y = autotable.get_current_pos(vbox)
1516 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1517 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1518 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1519 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1521 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1522 $vbox2widgets[widget_up][:textview].grab_focus
1529 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1531 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1532 $vbox2widgets[widget_down][:textview].grab_focus
1539 if event.keyval == Gdk::Keyval::GDK_Left
1542 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1549 rotate_and_cleanup.call(-90)
1552 if event.keyval == Gdk::Keyval::GDK_Right
1553 next_ = autotable.get_next_widget(vbox)
1554 if next_ && autotable.get_current_pos(next_)[0] > x
1556 $vbox2widgets[next_][:textview].grab_focus
1563 rotate_and_cleanup.call(90)
1566 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1569 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1570 view_element(filename, { :delete => delete })
1573 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1576 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1580 !propagate #- propagate if needed
1583 $ignore_next_release = false
1584 evtbox.signal_connect('button-press-event') { |w, event|
1585 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1586 if event.state & Gdk::Window::BUTTON3_MASK != 0
1587 #- gesture redo: hold right mouse button then click left mouse button
1588 $config['nogestures'] or perform_redo
1589 $ignore_next_release = true
1591 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1593 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1595 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1596 elsif $enhance.active?
1597 enhance_and_cleanup.call
1598 elsif $delete.active?
1602 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1605 $button1_pressed_autotable = true
1606 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1607 if event.state & Gdk::Window::BUTTON1_MASK != 0
1608 #- gesture undo: hold left mouse button then click right mouse button
1609 $config['nogestures'] or perform_undo
1610 $ignore_next_release = true
1612 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1613 view_element(filename, { :delete => delete })
1618 evtbox.signal_connect('button-release-event') { |w, event|
1619 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1620 if !$ignore_next_release
1621 x, y = autotable.get_current_pos(vbox)
1622 next_ = autotable.get_next_widget(vbox)
1623 popup_thumbnail_menu(event, ['delete'], fullpath, type, rexml_thread_protect { $xmldir.elements["*[@filename='#{filename}']"] }, '',
1624 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1625 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1626 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1627 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1628 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1629 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1631 $ignore_next_release = false
1632 $gesture_press = nil
1637 #- handle reordering with drag and drop
1638 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1639 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1640 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1641 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1644 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1646 #- mouse gesture first (dnd disables button-release-event)
1647 if $gesture_press && $gesture_press[:filename] == filename
1648 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1649 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1650 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1651 rotate_and_cleanup.call(angle)
1652 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1654 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1655 msg 3, "gesture delete: click-drag right button to the bottom"
1657 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1662 ctxt.targets.each { |target|
1663 if target.name == 'reorder-elements'
1664 move_dnd = proc { |from,to|
1667 autotable.move(from, to)
1668 save_undo(_("reorder"),
1671 autotable.move(to - 1, from)
1673 autotable.move(to, from + 1)
1675 $notebook.set_page(1)
1677 autotable.move(from, to)
1678 $notebook.set_page(1)
1683 if $multiple_dnd.size == 0
1684 move_dnd.call(selection_data.data.to_i,
1685 autotable.get_current_number(vbox))
1687 UndoHandler.begin_batch
1688 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1690 #- need to update current position between each call
1691 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1692 autotable.get_current_number(vbox))
1694 UndoHandler.end_batch
1705 def create_auto_table
1707 $autotable = Gtk::AutoTable.new(5)
1709 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1710 thumbnails_vb = Gtk::VBox.new(false, 5)
1712 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1713 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1714 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1715 thumbnails_vb.add($autotable)
1717 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1718 $autotable_sw.add_with_viewport(thumbnails_vb)
1720 #- follows stuff for handling multiple elements selection
1721 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1723 update_selected = proc {
1724 $autotable.current_order.each { |path|
1725 w = $name2widgets[path][:evtbox].window
1726 xm = w.position[0] + w.size[0]/2
1727 ym = w.position[1] + w.size[1]/2
1728 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1729 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1730 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1731 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1734 if $selected_elements[path] && ! $selected_elements[path][:keep]
1735 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))
1736 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1737 $selected_elements.delete(path)
1742 $autotable.signal_connect('realize') { |w,e|
1743 gc = Gdk::GC.new($autotable.window)
1744 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1745 gc.function = Gdk::GC::INVERT
1746 #- autoscroll handling for DND and multiple selections
1747 Gtk.timeout_add(100) {
1748 if ! $autotable.window.nil?
1749 w, x, y, mask = $autotable.window.pointer
1750 if mask & Gdk::Window::BUTTON1_MASK != 0
1751 if y < $autotable_sw.vadjustment.value
1753 $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]])
1755 if $button1_pressed_autotable || press_x
1756 scroll_upper($autotable_sw, y)
1759 w, pos_x, pos_y = $autotable.window.pointer
1760 $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]])
1761 update_selected.call
1764 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1766 $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]])
1768 if $button1_pressed_autotable || press_x
1769 scroll_lower($autotable_sw, y)
1772 w, pos_x, pos_y = $autotable.window.pointer
1773 $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]])
1774 update_selected.call
1779 ! $autotable.window.nil?
1783 $autotable.signal_connect('button-press-event') { |w,e|
1785 if !$button1_pressed_autotable
1788 if e.state & Gdk::Window::SHIFT_MASK == 0
1789 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1790 $selected_elements = {}
1791 $statusbar.push(0, utf8(_("Nothing selected.")))
1793 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1795 set_mousecursor(Gdk::Cursor::TCROSS)
1799 $autotable.signal_connect('button-release-event') { |w,e|
1801 if $button1_pressed_autotable
1802 #- unselect all only now
1803 $multiple_dnd = $selected_elements.keys
1804 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1805 $selected_elements = {}
1806 $button1_pressed_autotable = false
1809 $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]])
1810 if $selected_elements.length > 0
1811 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1814 press_x = press_y = pos_x = pos_y = nil
1815 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1819 $autotable.signal_connect('motion-notify-event') { |w,e|
1822 $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]])
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]])
1827 update_selected.call
1833 def create_subalbums_page
1835 subalbums_hb = Gtk::HBox.new
1836 $subalbums_vb = Gtk::VBox.new(false, 5)
1837 subalbums_hb.pack_start($subalbums_vb, false, false)
1838 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1839 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1840 $subalbums_sw.add_with_viewport(subalbums_hb)
1843 def save_current_file
1849 ios = File.open($filename, "w")
1850 $xmldoc.write(ios, 0)
1852 rescue Iconv::IllegalSequence
1853 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1854 if ! ios.nil? && ! ios.closed?
1857 $xmldoc.xml_decl.encoding = 'UTF-8'
1858 ios = File.open($filename, "w")
1859 $xmldoc.write(ios, 0)
1870 def save_current_file_user
1871 save_tempfilename = $filename
1872 $filename = $orig_filename
1873 if ! save_current_file
1874 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1875 $filename = save_tempfilename
1879 $generated_outofline = false
1880 $filename = save_tempfilename
1882 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1883 $todelete.each { |f|
1888 def mark_document_as_dirty
1889 $xmldoc.elements.each('//dir') { |elem|
1890 elem.delete_attribute('already-generated')
1894 #- ret: true => ok false => cancel
1895 def ask_save_modifications(msg1, msg2, *options)
1897 options = options.size > 0 ? options[0] : {}
1899 if options[:disallow_cancel]
1900 dialog = Gtk::Dialog.new(msg1,
1902 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1903 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1904 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1906 dialog = Gtk::Dialog.new(msg1,
1908 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1909 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1910 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1911 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1913 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1914 dialog.vbox.add(Gtk::Label.new(msg2))
1915 dialog.window_position = Gtk::Window::POS_CENTER
1918 dialog.run { |response|
1920 if response == Gtk::Dialog::RESPONSE_YES
1921 if ! save_current_file_user
1922 return ask_save_modifications(msg1, msg2, options)
1925 #- if we have generated an album but won't save modifications, we must remove
1926 #- already-generated markers in original file
1927 if $generated_outofline
1929 $xmldoc = REXML::Document.new(File.new($orig_filename))
1930 mark_document_as_dirty
1931 ios = File.open($orig_filename, "w")
1932 $xmldoc.write(ios, 0)
1935 puts "exception: #{$!}"
1939 if response == Gtk::Dialog::RESPONSE_CANCEL
1942 $todelete = [] #- unconditionally clear the list of images/videos to delete
1948 def try_quit(*options)
1949 if ask_save_modifications(utf8(_("Save before quitting?")),
1950 utf8(_("Do you want to save your changes before quitting?")),
1956 def show_popup(parent, msg, *options)
1957 dialog = Gtk::Dialog.new
1958 if options[0] && options[0][:title]
1959 dialog.title = options[0][:title]
1961 dialog.title = utf8(_("Booh message"))
1963 lbl = Gtk::Label.new
1964 if options[0] && options[0][:nomarkup]
1969 if options[0] && options[0][:centered]
1970 lbl.set_justify(Gtk::Justification::CENTER)
1972 if options[0] && options[0][:selectable]
1973 lbl.selectable = true
1975 if options[0] && options[0][:topwidget]
1976 dialog.vbox.add(options[0][:topwidget])
1978 if options[0] && options[0][:scrolled]
1979 sw = Gtk::ScrolledWindow.new(nil, nil)
1980 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1981 sw.add_with_viewport(lbl)
1983 dialog.set_default_size(500, 600)
1985 dialog.vbox.add(lbl)
1986 dialog.set_default_size(200, 120)
1988 if options[0] && options[0][:okcancel]
1989 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1991 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1993 if options[0] && options[0][:pos_centered]
1994 dialog.window_position = Gtk::Window::POS_CENTER
1996 dialog.window_position = Gtk::Window::POS_MOUSE
1999 if options[0] && options[0][:linkurl]
2000 linkbut = Gtk::Button.new('')
2001 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
2002 linkbut.signal_connect('clicked') {
2003 open_url(options[0][:linkurl])
2004 dialog.response(Gtk::Dialog::RESPONSE_OK)
2005 set_mousecursor_normal
2007 linkbut.relief = Gtk::RELIEF_NONE
2008 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
2009 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
2010 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
2015 if !options[0] || !options[0][:not_transient]
2016 dialog.transient_for = parent
2017 dialog.run { |response|
2019 if options[0] && options[0][:okcancel]
2020 return response == Gtk::Dialog::RESPONSE_OK
2024 dialog.signal_connect('response') { dialog.destroy }
2028 def set_mainwindow_title(progress)
2029 filename = $orig_filename || $filename
2032 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
2034 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
2038 $main_window.title = 'booh - ' + File.basename(filename)
2040 $main_window.title = 'booh'
2045 def backend_wait_message(parent, msg, infopipe_path, mode)
2047 w.set_transient_for(parent)
2050 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2051 vb.pack_start(Gtk::Label.new(msg), false, false)
2053 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
2054 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
2055 if mode != 'one dir scan'
2056 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2058 if mode == 'web-album'
2059 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2060 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2062 vb.pack_start(Gtk::HSeparator.new, false, false)
2064 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2065 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2066 vb.pack_end(bottom, false, false)
2069 update_progression_title_pb1 = proc {
2070 if mode == 'web-album'
2071 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2072 elsif mode != 'one dir scan'
2073 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2075 set_mainwindow_title(pb1_1.fraction)
2079 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2080 refresh_thread = Thread.new {
2081 directories_counter = 0
2082 while line = infopipe.gets
2083 if line =~ /^directories: (\d+), sizes: (\d+)/
2084 directories = $1.to_f + 1
2086 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2087 elements = $3.to_f + 1
2088 if mode == 'web-album'
2092 gtk_thread_protect { pb1_1.fraction = 0 }
2093 if mode != 'one dir scan'
2094 newtext = utf8(full_src_dir_to_rel($1, $2))
2095 newtext = '/' if newtext == ''
2096 gtk_thread_protect { pb1_2.text = newtext }
2097 directories_counter += 1
2098 gtk_thread_protect {
2099 pb1_2.fraction = directories_counter / directories
2100 update_progression_title_pb1.call
2103 elsif line =~ /^processing element$/
2104 element_counter += 1
2105 gtk_thread_protect {
2106 pb1_1.fraction = element_counter / elements
2107 update_progression_title_pb1.call
2109 elsif line =~ /^processing size$/
2110 element_counter += 1
2111 gtk_thread_protect {
2112 pb1_1.fraction = element_counter / elements
2113 update_progression_title_pb1.call
2115 elsif line =~ /^finished processing sizes$/
2116 gtk_thread_protect { pb1_1.fraction = 1 }
2117 elsif line =~ /^creating index.html$/
2118 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2119 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2120 directories_counter = 0
2121 elsif line =~ /^index.html: (.+)\|(.+)/
2122 newtext = utf8(full_src_dir_to_rel($1, $2))
2123 newtext = '/' if newtext == ''
2124 gtk_thread_protect { pb2.text = newtext }
2125 directories_counter += 1
2126 gtk_thread_protect {
2127 pb2.fraction = directories_counter / directories
2128 set_mainwindow_title(0.9 + pb2.fraction / 10)
2130 elsif line =~ /^die: (.*)$/
2137 w.signal_connect('delete-event') { w.destroy }
2138 w.signal_connect('destroy') {
2139 Thread.kill(refresh_thread)
2140 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2143 File.delete(infopipe_path)
2145 set_mainwindow_title(nil)
2147 w.window_position = Gtk::Window::POS_CENTER
2153 def call_backend(cmd, waitmsg, mode, params)
2154 pipe = Tempfile.new("boohpipe")
2155 Thread.critical = true
2158 system("mkfifo #{path}")
2159 Thread.critical = false
2160 cmd += " --info-pipe #{path}"
2161 button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2166 id, exitstatus = Process.waitpid2(pid)
2167 gtk_thread_protect { w8.destroy }
2169 if params[:successmsg]
2170 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2172 if params[:closure_after]
2173 gtk_thread_protect(¶ms[:closure_after])
2175 elsif exitstatus == 15
2176 #- say nothing, user aborted
2178 gtk_thread_protect { show_popup($main_window,
2179 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2185 button.signal_connect('clicked') {
2186 Process.kill('SIGTERM', pid)
2190 def save_changes(*forced)
2191 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2195 $xmldir.delete_attribute('already-generated')
2197 propagate_children = proc { |xmldir|
2198 if xmldir.attributes['subdirs-caption']
2199 xmldir.delete_attribute('already-generated')
2201 xmldir.elements.each('dir') { |element|
2202 propagate_children.call(element)
2206 if $xmldir.child_byname_notattr('dir', 'deleted')
2207 new_title = $subalbums_title.buffer.text
2208 if new_title != $xmldir.attributes['subdirs-caption']
2209 parent = $xmldir.parent
2210 if parent.name == 'dir'
2211 parent.delete_attribute('already-generated')
2213 propagate_children.call($xmldir)
2215 $xmldir.add_attribute('subdirs-caption', new_title)
2216 $xmldir.elements.each('dir') { |element|
2217 if !element.attributes['deleted']
2218 path = element.attributes['path']
2219 newtext = $subalbums_edits[path][:editzone].buffer.text
2220 if element.attributes['subdirs-caption']
2221 if element.attributes['subdirs-caption'] != newtext
2222 propagate_children.call(element)
2224 element.add_attribute('subdirs-caption', newtext)
2225 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2227 if element.attributes['thumbnails-caption'] != newtext
2228 element.delete_attribute('already-generated')
2230 element.add_attribute('thumbnails-caption', newtext)
2231 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2237 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2238 if $xmldir.attributes['thumbnails-caption']
2239 path = $xmldir.attributes['path']
2240 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2242 elsif $xmldir.attributes['thumbnails-caption']
2243 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2246 if $xmldir.attributes['thumbnails-caption']
2247 if edit = $subalbums_edits[$xmldir.attributes['path']]
2248 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2252 #- remove and reinsert elements to reflect new ordering
2255 $xmldir.elements.each { |element|
2256 if element.name == 'image' || element.name == 'video'
2257 saves[element.attributes['filename']] = element.remove
2261 $autotable.current_order.each { |path|
2262 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2263 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2266 saves.each_key { |path|
2267 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2268 chld.add_attribute('deleted', 'true')
2272 def sort_by_exif_date
2276 rexml_thread_protect {
2277 $xmldir.elements.each { |element|
2278 if element.name == 'image' || element.name == 'video'
2279 current_order << element.attributes['filename']
2284 #- look for EXIF dates
2287 if current_order.size > 20
2289 w.set_transient_for($main_window)
2291 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2292 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2293 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2294 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2295 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2296 vb.pack_end(bottom, false, false)
2298 w.signal_connect('delete-event') { w.destroy }
2299 w.window_position = Gtk::Window::POS_CENTER
2303 b.signal_connect('clicked') { aborted = true }
2305 current_order.each { |f|
2307 if entry2type(f) == 'image'
2309 pb.fraction = i.to_f / current_order.size
2310 Gtk.main_iteration while Gtk.events_pending?
2311 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2313 dates[f] = date_time
2326 current_order.each { |f|
2327 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2329 dates[f] = date_time
2335 rexml_thread_protect {
2336 $xmldir.elements.each { |element|
2337 if element.name == 'image' || element.name == 'video'
2338 saves[element.attributes['filename']] = element.remove
2343 neworder = smartsort(current_order, dates)
2345 rexml_thread_protect {
2347 $xmldir.add_element(saves[f].name, saves[f].attributes)
2351 #- let the auto-table reflect new ordering
2355 def remove_all_captions
2358 $autotable.current_order.each { |path|
2359 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2360 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2362 save_undo(_("remove all captions"),
2364 texts.each_key { |key|
2365 $name2widgets[key][:textview].buffer.text = texts[key]
2367 $notebook.set_page(1)
2369 texts.each_key { |key|
2370 $name2widgets[key][:textview].buffer.text = ''
2372 $notebook.set_page(1)
2378 $selected_elements.each_key { |path|
2379 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2385 $selected_elements = {}
2389 $undo_tb.sensitive = $undo_mb.sensitive = false
2390 $redo_tb.sensitive = $redo_mb.sensitive = false
2396 $subalbums_vb.children.each { |chld|
2397 $subalbums_vb.remove(chld)
2399 $subalbums = Gtk::Table.new(0, 0, true)
2400 current_y_sub_albums = 0
2402 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2403 $subalbums_edits = {}
2404 subalbums_counter = 0
2405 subalbums_edits_bypos = {}
2407 add_subalbum = proc { |xmldir, counter|
2408 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2409 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2410 if xmldir == $xmldir
2411 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2412 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2413 caption = xmldir.attributes['thumbnails-caption']
2414 infotype = 'thumbnails'
2416 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2417 captionfile, caption = find_subalbum_caption_info(xmldir)
2418 infotype = find_subalbum_info_type(xmldir)
2420 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2421 hbox = Gtk::HBox.new
2422 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2424 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2427 my_gen_real_thumbnail = proc {
2428 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2431 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2432 f.add(img = Gtk::Image.new)
2433 my_gen_real_thumbnail.call
2435 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2437 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2438 $subalbums.attach(hbox,
2439 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2441 frame, textview = create_editzone($subalbums_sw, 0, img)
2442 textview.buffer.text = caption
2443 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2444 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2446 change_image = proc {
2447 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2449 Gtk::FileChooser::ACTION_OPEN,
2451 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2452 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2453 fc.transient_for = $main_window
2454 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))
2455 f.add(preview_img = Gtk::Image.new)
2457 fc.signal_connect('update-preview') { |w|
2459 if fc.preview_filename
2460 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2461 fc.preview_widget_active = true
2463 rescue Gdk::PixbufError
2464 fc.preview_widget_active = false
2467 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2469 old_file = captionfile
2470 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2471 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2472 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2473 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2475 new_file = fc.filename
2476 msg 3, "new captionfile is: #{fc.filename}"
2477 perform_changefile = proc {
2478 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2479 $modified_pixbufs.delete(thumbnail_file)
2480 xmldir.delete_attribute("#{infotype}-rotate")
2481 xmldir.delete_attribute("#{infotype}-color-swap")
2482 xmldir.delete_attribute("#{infotype}-enhance")
2483 xmldir.delete_attribute("#{infotype}-seektime")
2484 my_gen_real_thumbnail.call
2486 perform_changefile.call
2488 save_undo(_("change caption file for sub-album"),
2490 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2491 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2492 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2493 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2494 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2495 my_gen_real_thumbnail.call
2496 $notebook.set_page(0)
2498 perform_changefile.call
2499 $notebook.set_page(0)
2507 if File.exists?(thumbnail_file)
2508 File.delete(thumbnail_file)
2510 my_gen_real_thumbnail.call
2513 rotate_and_cleanup = proc { |angle|
2514 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2515 if File.exists?(thumbnail_file)
2516 File.delete(thumbnail_file)
2520 move = proc { |direction|
2523 save_changes('forced')
2524 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2525 if direction == 'up'
2526 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2527 subalbums_edits_bypos[oldpos - 1][:position] += 1
2529 if direction == 'down'
2530 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2531 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2533 if direction == 'top'
2534 for i in 1 .. oldpos - 1
2535 subalbums_edits_bypos[i][:position] += 1
2537 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2539 if direction == 'bottom'
2540 for i in oldpos + 1 .. subalbums_counter
2541 subalbums_edits_bypos[i][:position] -= 1
2543 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2547 $xmldir.elements.each('dir') { |element|
2548 if (!element.attributes['deleted'])
2549 elems << [ element.attributes['path'], element.remove ]
2552 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2553 each { |e| $xmldir.add_element(e[1]) }
2554 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2555 $xmldir.elements.each('descendant::dir') { |elem|
2556 elem.delete_attribute('already-generated')
2559 sel = $albums_tv.selection.selected_rows
2561 populate_subalbums_treeview(false)
2562 $albums_tv.selection.select_path(sel[0])
2565 color_swap_and_cleanup = proc {
2566 perform_color_swap_and_cleanup = proc {
2567 color_swap(xmldir, "#{infotype}-")
2568 my_gen_real_thumbnail.call
2570 perform_color_swap_and_cleanup.call
2572 save_undo(_("color swap"),
2574 perform_color_swap_and_cleanup.call
2575 $notebook.set_page(0)
2577 perform_color_swap_and_cleanup.call
2578 $notebook.set_page(0)
2583 change_seektime_and_cleanup = proc {
2584 if values = ask_new_seektime(xmldir, "#{infotype}-")
2585 perform_change_seektime_and_cleanup = proc { |val|
2586 change_seektime(xmldir, "#{infotype}-", val)
2587 my_gen_real_thumbnail.call
2589 perform_change_seektime_and_cleanup.call(values[:new])
2591 save_undo(_("specify seektime"),
2593 perform_change_seektime_and_cleanup.call(values[:old])
2594 $notebook.set_page(0)
2596 perform_change_seektime_and_cleanup.call(values[:new])
2597 $notebook.set_page(0)
2603 whitebalance_and_cleanup = proc {
2604 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2605 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2606 perform_change_whitebalance_and_cleanup = proc { |val|
2607 change_whitebalance(xmldir, "#{infotype}-", val)
2608 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2609 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2610 if File.exists?(thumbnail_file)
2611 File.delete(thumbnail_file)
2614 perform_change_whitebalance_and_cleanup.call(values[:new])
2616 save_undo(_("fix white balance"),
2618 perform_change_whitebalance_and_cleanup.call(values[:old])
2619 $notebook.set_page(0)
2621 perform_change_whitebalance_and_cleanup.call(values[:new])
2622 $notebook.set_page(0)
2628 gammacorrect_and_cleanup = proc {
2629 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2630 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2631 perform_change_gammacorrect_and_cleanup = proc { |val|
2632 change_gammacorrect(xmldir, "#{infotype}-", val)
2633 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2634 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2635 if File.exists?(thumbnail_file)
2636 File.delete(thumbnail_file)
2639 perform_change_gammacorrect_and_cleanup.call(values[:new])
2641 save_undo(_("gamma correction"),
2643 perform_change_gammacorrect_and_cleanup.call(values[:old])
2644 $notebook.set_page(0)
2646 perform_change_gammacorrect_and_cleanup.call(values[:new])
2647 $notebook.set_page(0)
2653 enhance_and_cleanup = proc {
2654 perform_enhance_and_cleanup = proc {
2655 enhance(xmldir, "#{infotype}-")
2656 my_gen_real_thumbnail.call
2659 perform_enhance_and_cleanup.call
2661 save_undo(_("enhance"),
2663 perform_enhance_and_cleanup.call
2664 $notebook.set_page(0)
2666 perform_enhance_and_cleanup.call
2667 $notebook.set_page(0)
2672 evtbox.signal_connect('button-press-event') { |w, event|
2673 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2675 rotate_and_cleanup.call(90)
2677 rotate_and_cleanup.call(-90)
2678 elsif $enhance.active?
2679 enhance_and_cleanup.call
2682 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2683 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2684 { :forbid_left => true, :forbid_right => true,
2685 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2686 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2687 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2688 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2689 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2691 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2696 evtbox.signal_connect('button-press-event') { |w, event|
2697 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2701 evtbox.signal_connect('button-release-event') { |w, event|
2702 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2703 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2704 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2705 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2706 msg 3, "gesture rotate: #{angle}"
2707 rotate_and_cleanup.call(angle)
2710 $gesture_press = nil
2713 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2714 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2715 current_y_sub_albums += 1
2718 if $xmldir.child_byname_notattr('dir', 'deleted')
2720 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2721 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2722 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2723 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2724 #- this album image/caption
2725 if $xmldir.attributes['thumbnails-caption']
2726 add_subalbum.call($xmldir, 0)
2729 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2730 $xmldir.elements.each { |element|
2731 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2732 #- element (image or video) of this album
2733 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2734 msg 3, "dest_img: #{dest_img}"
2735 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2736 total[element.name] += 1
2738 if element.name == 'dir' && !element.attributes['deleted']
2739 #- sub-album image/caption
2740 add_subalbum.call(element, subalbums_counter += 1)
2741 total[element.name] += 1
2744 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2745 total['image'], total['video'], total['dir'] ]))
2746 $subalbums_vb.add($subalbums)
2747 $subalbums_vb.show_all
2749 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2750 $notebook.get_tab_label($autotable_sw).sensitive = false
2751 $notebook.set_page(0)
2752 $thumbnails_title.buffer.text = ''
2754 $notebook.get_tab_label($autotable_sw).sensitive = true
2755 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2758 if !$xmldir.child_byname_notattr('dir', 'deleted')
2759 $notebook.get_tab_label($subalbums_sw).sensitive = false
2760 $notebook.set_page(1)
2762 $notebook.get_tab_label($subalbums_sw).sensitive = true
2766 def pixbuf_or_nil(filename)
2768 return Gdk::Pixbuf.new(filename)
2774 def theme_choose(current)
2775 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2777 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2778 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2779 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2781 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2782 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2783 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2784 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2785 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2786 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2787 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2788 treeview.signal_connect('button-press-event') { |w, event|
2789 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2790 dialog.response(Gtk::Dialog::RESPONSE_OK)
2794 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2796 ([ $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|
2799 iter[0] = File.basename(dir)
2800 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2801 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2802 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2803 if File.basename(dir) == current
2804 treeview.selection.select_iter(iter)
2807 dialog.set_default_size(-1, 500)
2808 dialog.vbox.show_all
2810 dialog.run { |response|
2811 iter = treeview.selection.selected
2813 if response == Gtk::Dialog::RESPONSE_OK && iter
2814 return model.get_value(iter, 0)
2820 def show_password_protections
2821 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2822 child_iter = $albums_iters[xmldir.attributes['path']]
2823 if xmldir.attributes['password-protect']
2824 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2825 already_protected = true
2826 elsif already_protected
2827 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2829 pix = pix.saturate_and_pixelate(1, true)
2835 xmldir.elements.each('dir') { |elem|
2836 if !elem.attributes['deleted']
2837 examine_dir_elem.call(child_iter, elem, already_protected)
2841 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2844 def populate_subalbums_treeview(select_first)
2848 $subalbums_vb.children.each { |chld|
2849 $subalbums_vb.remove(chld)
2852 source = $xmldoc.root.attributes['source']
2853 msg 3, "source: #{source}"
2855 xmldir = $xmldoc.elements['//dir']
2856 if !xmldir || xmldir.attributes['path'] != source
2857 msg 1, _("Corrupted booh file...")
2861 append_dir_elem = proc { |parent_iter, xmldir|
2862 child_iter = $albums_ts.append(parent_iter)
2863 child_iter[0] = File.basename(xmldir.attributes['path'])
2864 child_iter[1] = xmldir.attributes['path']
2865 $albums_iters[xmldir.attributes['path']] = child_iter
2866 msg 3, "puttin location: #{xmldir.attributes['path']}"
2867 xmldir.elements.each('dir') { |elem|
2868 if !elem.attributes['deleted']
2869 append_dir_elem.call(child_iter, elem)
2873 append_dir_elem.call(nil, xmldir)
2874 show_password_protections
2876 $albums_tv.expand_all
2878 $albums_tv.selection.select_iter($albums_ts.iter_first)
2882 def select_current_theme
2883 select_theme($xmldoc.root.attributes['theme'],
2884 $xmldoc.root.attributes['limit-sizes'],
2885 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2886 $xmldoc.root.attributes['thumbnails-per-row'])
2889 def open_file(filename)
2893 $current_path = nil #- invalidate
2894 $modified_pixbufs = {}
2897 $subalbums_vb.children.each { |chld|
2898 $subalbums_vb.remove(chld)
2901 if !File.exists?(filename)
2902 return utf8(_("File not found."))
2906 $xmldoc = REXML::Document.new(File.new(filename))
2911 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2912 if entry2type(filename).nil?
2913 return utf8(_("Not a booh file!"))
2915 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."))
2919 if !source = $xmldoc.root.attributes['source']
2920 return utf8(_("Corrupted booh file..."))
2923 if !dest = $xmldoc.root.attributes['destination']
2924 return utf8(_("Corrupted booh file..."))
2927 if !theme = $xmldoc.root.attributes['theme']
2928 return utf8(_("Corrupted booh file..."))
2931 if $xmldoc.root.attributes['version'] < '0.9.0'
2932 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2933 mark_document_as_dirty
2934 if $xmldoc.root.attributes['version'] < '0.8.4'
2935 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2936 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2937 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2938 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2939 if old_dest_dir != new_dest_dir
2940 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2942 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2943 xmldir.elements.each { |element|
2944 if %w(image video).include?(element.name) && !element.attributes['deleted']
2945 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2946 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2947 Dir[old_name + '*'].each { |file|
2948 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2949 file != new_file and sys("mv '#{file}' '#{new_file}'")
2952 if element.name == 'dir' && !element.attributes['deleted']
2953 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2954 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2955 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2959 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2963 $xmldoc.root.add_attribute('version', $VERSION)
2966 select_current_theme
2968 $filename = filename
2969 set_mainwindow_title(nil)
2970 $default_size['thumbnails'] =~ /(.*)x(.*)/
2971 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2972 $albums_thumbnail_size =~ /(.*)x(.*)/
2973 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2975 populate_subalbums_treeview(true)
2977 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
2981 def open_file_user(filename)
2982 result = open_file(filename)
2984 $config['last-opens'] ||= []
2985 if $config['last-opens'][-1] != utf8(filename)
2986 $config['last-opens'] << utf8(filename)
2988 $orig_filename = $filename
2989 $main_window.title = 'booh - ' + File.basename($orig_filename)
2990 tmp = Tempfile.new("boohtemp")
2991 Thread.critical = true
2992 $filename = tmp.path
2995 ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
2996 Thread.critical = false
2998 $tempfiles << $filename << "#{$filename}.backup"
3000 $orig_filename = nil
3006 if !ask_save_modifications(utf8(_("Save this album?")),
3007 utf8(_("Do you want to save the changes to this album?")),
3008 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3011 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
3013 Gtk::FileChooser::ACTION_OPEN,
3015 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3016 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3017 fc.set_current_folder(File.expand_path("~/.booh"))
3018 fc.transient_for = $main_window
3021 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3022 push_mousecursor_wait(fc)
3023 msg = open_file_user(fc.filename)
3038 def additional_booh_options
3041 options += "--mproc #{$config['mproc'].to_i} "
3043 options += "--comments-format '#{$config['comments-format']}' "
3044 if $config['transcode-videos']
3045 options += "--transcode-videos '#{$config['transcode-videos']}' "
3050 def ask_multi_languages(value)
3052 spl = value.split(',')
3053 value = [ spl[0..-2], spl[-1] ]
3056 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
3059 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3060 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3062 lbl = Gtk::Label.new
3064 _("You can choose to activate <b>multi-languages</b> support for this web-album
3065 (it will work only if you publish your web-album on an Apache web-server). This will
3066 use the MultiViews feature of Apache; the pages will be served according to the
3067 value of the Accept-Language HTTP header sent by the web browsers, so that people
3068 with different languages preferences will be able to browse your web-album with
3069 navigation in their language (if language is available).
3072 dialog.vbox.add(lbl)
3073 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3074 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3075 add(languages = Gtk::Button.new))))
3077 pick_languages = proc {
3078 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3081 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3082 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3084 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3085 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3087 SUPPORTED_LANGUAGES.each { |lang|
3088 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3089 if ! value.nil? && value[0].include?(lang)
3095 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3096 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3097 fallback_language = nil
3098 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3099 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3100 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3101 fbl_rb.active = true
3102 fallback_language = SUPPORTED_LANGUAGES[0]
3104 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3105 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3106 rb.signal_connect('clicked') { fallback_language = lang }
3107 if ! value.nil? && value[1] == lang
3112 dialog2.window_position = Gtk::Window::POS_MOUSE
3116 dialog2.run { |response|
3118 if resp == Gtk::Dialog::RESPONSE_OK
3120 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3121 value[1] = fallback_language
3122 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3129 languages.signal_connect('clicked') {
3132 dialog.window_position = Gtk::Window::POS_MOUSE
3136 rb_yes.active = true
3137 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3139 rb_no.signal_connect('clicked') {
3143 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3156 dialog.run { |response|
3161 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3163 return [ true, nil ]
3165 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3174 if !ask_save_modifications(utf8(_("Save this album?")),
3175 utf8(_("Do you want to save the changes to this album?")),
3176 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3179 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3181 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3182 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3183 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3185 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3186 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3187 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3188 tbl.attach(src = Gtk::Entry.new,
3189 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3190 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3191 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3192 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3193 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3194 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3195 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3196 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3197 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3198 tbl.attach(dest = Gtk::Entry.new,
3199 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3200 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3201 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3202 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3203 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3204 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3205 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3206 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3207 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3209 tooltips = Gtk::Tooltips.new
3210 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3211 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3212 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3213 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3214 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3215 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3216 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)
3217 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3218 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3219 nperpage_model = Gtk::ListStore.new(String, String)
3220 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3221 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3222 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3223 nperpagecombo.set_attributes(crt, { :markup => 0 })
3224 iter = nperpage_model.append
3225 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3227 [ 12, 20, 30, 40, 50 ].each { |v|
3228 iter = nperpage_model.append
3229 iter[0] = iter[1] = v.to_s
3231 nperpagecombo.active = 0
3233 multilanguages_value = nil
3234 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3235 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3236 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)
3237 multilanguages.signal_connect('clicked') {
3238 retval = ask_multi_languages(multilanguages_value)
3240 multilanguages_value = retval[1]
3242 if multilanguages_value
3243 ml_label.text = utf8(_("Multi-languages: enabled."))
3245 ml_label.text = utf8(_("Multi-languages: disabled."))
3248 if $config['default-multi-languages']
3249 multilanguages_value = $config['default-multi-languages']
3250 ml_label.text = utf8(_("Multi-languages: enabled."))
3252 ml_label.text = utf8(_("Multi-languages: disabled."))
3255 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3256 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3257 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)
3258 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3259 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3260 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)
3262 src_nb_calculated_for = ''
3264 process_src_nb = proc {
3265 if src.text != src_nb_calculated_for
3266 src_nb_calculated_for = src.text
3268 Thread.kill(src_nb_thread)
3271 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3272 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3274 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3275 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3276 src_nb_thread = Thread.new {
3277 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3278 total = { 'image' => 0, 'video' => 0, nil => 0 }
3279 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3280 if File.basename(dir) =~ /^\./
3284 Dir.entries(dir.chomp).each { |file|
3285 total[entry2type(file)] += 1
3287 rescue Errno::EACCES, Errno::ENOENT
3291 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3295 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3298 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3304 timeout_src_nb = Gtk.timeout_add(100) {
3308 src_browse.signal_connect('clicked') {
3309 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3311 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3313 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3314 fc.transient_for = $main_window
3315 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3316 src.text = utf8(fc.filename)
3318 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3323 dest_browse.signal_connect('clicked') {
3324 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3326 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3328 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3329 fc.transient_for = $main_window
3330 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3331 dest.text = utf8(fc.filename)
3336 conf_browse.signal_connect('clicked') {
3337 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3339 Gtk::FileChooser::ACTION_SAVE,
3341 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3342 fc.transient_for = $main_window
3343 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3344 fc.set_current_folder(File.expand_path("~/.booh"))
3345 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3346 conf.text = utf8(fc.filename)
3353 recreate_theme_config = proc {
3354 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3356 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3357 $images_size.each { |s|
3358 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3362 tooltips.set_tip(cb, utf8(s['description']), nil)
3363 theme_sizes << { :widget => cb, :value => s['name'] }
3365 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3366 tooltips = Gtk::Tooltips.new
3367 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3368 theme_sizes << { :widget => cb, :value => 'original' }
3371 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3374 $allowed_N_values.each { |n|
3376 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3378 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3380 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3384 nperrows << { :widget => rb, :value => n }
3386 nperrowradios.show_all
3388 recreate_theme_config.call
3390 theme_button.signal_connect('clicked') {
3391 if newtheme = theme_choose(theme_button.label)
3392 theme_button.label = newtheme
3393 recreate_theme_config.call
3397 dialog.vbox.add(frame1)
3398 dialog.vbox.add(frame2)
3404 dialog.run { |response|
3405 if response == Gtk::Dialog::RESPONSE_OK
3406 srcdir = from_utf8_safe(src.text)
3407 destdir = from_utf8_safe(dest.text)
3408 confpath = from_utf8_safe(conf.text)
3409 if src.text != '' && srcdir == ''
3410 show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3412 elsif !File.directory?(srcdir)
3413 show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3415 elsif dest.text != '' && destdir == ''
3416 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3418 elsif destdir != make_dest_filename(destdir)
3419 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3421 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3422 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3423 inside it will be permanently removed before creating the web-album!
3424 Are you sure you want to continue?")), { :okcancel => true })
3426 elsif File.exists?(destdir) && !File.directory?(destdir)
3427 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3429 elsif conf.text == ''
3430 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3432 elsif conf.text != '' && confpath == ''
3433 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3435 elsif File.directory?(confpath)
3436 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3438 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3439 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3441 system("mkdir '#{destdir}'")
3442 if !File.directory?(destdir)
3443 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3455 srcdir = from_utf8(src.text)
3456 destdir = from_utf8(dest.text)
3457 configskel = File.expand_path(from_utf8(conf.text))
3458 theme = theme_button.label
3459 #- some sort of automatic theme preference
3460 $config['default-theme'] = theme
3461 $config['default-multi-languages'] = multilanguages_value
3462 $config['default-optimize32'] = optimize432.active?.to_s
3463 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3464 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3465 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3466 opt432 = optimize432.active?
3467 madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3468 indexlink = indexlinkentry.text.gsub('\'', ''')
3471 Thread.kill(src_nb_thread)
3472 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3475 Gtk.timeout_remove(timeout_src_nb)
3478 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3479 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3480 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3481 (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
3482 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3483 utf8(_("Please wait while scanning source directory...")),
3485 { :closure_after => proc {
3486 open_file_user(configskel)
3487 $main_window.urgency_hint = true
3493 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3495 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3496 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3497 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3499 source = $xmldoc.root.attributes['source']
3500 dest = $xmldoc.root.attributes['destination']
3501 theme = $xmldoc.root.attributes['theme']
3502 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3503 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3504 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3505 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3507 limit_sizes = limit_sizes.split(/,/)
3509 madewith = ($xmldoc.root.attributes['made-with'] || '').gsub(''', '\'')
3510 indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub(''', '\'')
3511 save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3513 tooltips = Gtk::Tooltips.new
3514 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3515 tbl.attach(Gtk::Label.new(utf8(_("Directory of source photos/videos: "))),
3516 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3517 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3518 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3519 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3520 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)