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'
40 require 'booh/Synchronizator'
45 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
46 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
47 [ '--version', '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
50 #- default values for some globals
52 $xmlaccesslock = Object.new
55 $ignore_videos = false
56 $button1_pressed_autotable = false
57 $generated_outofline = false
60 puts _("Usage: %s [OPTION]...") % File.basename($0)
62 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
67 parser = GetoptLong.new
68 parser.set_options(*$options.collect { |ary| ary[0..2] })
70 parser.each_option do |name, arg|
77 puts _("Booh version %s
79 Copyright (c) 2005-2008 Guillaume Cottenceau.
80 This is free software; see the source for copying conditions. There is NO
81 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
85 when '--verbose-level'
86 $verbose_level = arg.to_i
99 $config_file = File.expand_path('~/.booh-gui-rc')
100 if File.readable?($config_file)
101 $xmldoc = Synchronizator.new(REXML::Document.new(File.new($config_file)), $xmlaccesslock)
102 $xmldoc.root.elements.each { |element|
103 txt = element.get_text
105 if txt.value =~ /~~~/ || element.name == 'last-opens'
106 $config[element.name] = txt.value.split(/~~~/)
108 $config[element.name] = txt.value
110 elsif element.elements.size == 0
111 $config[element.name] = ''
113 $config[element.name] = {}
114 element.each { |chld|
116 $config[element.name][chld.name] = txt ? txt.value : nil
121 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
122 $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
123 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
124 $config['comments-format'] ||= '%t'
125 if !FileTest.directory?(File.expand_path('~/.booh'))
126 system("mkdir ~/.booh")
128 if $config['mproc'].nil?
130 for line in IO.readlines('/proc/cpuinfo') do
131 line =~ /^processor/ and cpus += 1
134 $config['mproc'] = cpus
137 $config['rotate-set-exif'] ||= 'true'
143 if !system("which convert >/dev/null 2>/dev/null")
144 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
145 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
148 if !system("which identify >/dev/null 2>/dev/null")
149 show_popup($main_window, utf8(_("The program 'identify' is needed to get photos sizes and EXIF data. Please install it.
150 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
152 if !system("which exif >/dev/null 2>/dev/null")
153 show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
155 missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
157 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
160 viewer_binary = $config['video-viewer'].split.first
161 if viewer_binary && !File.executable?(viewer_binary)
162 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
163 You should fix this in Edit/Preferences so that you can view videos.
165 Problem was: '%s' is not an executable file.
166 Hint: don't forget to specify the full path to the executable,
167 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
171 def check_image_editor
172 image_editor_binary = $config['image-editor'].split.first
173 if image_editor_binary && !File.executable?(image_editor_binary)
174 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
175 You should fix this in Edit/Preferences so that you can edit photos externally.
177 Problem was: '%s' is not an executable file.
178 Hint: don't forget to specify the full path to the executable,
179 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
187 if $config['last-opens'] && $config['last-opens'].size > 10
188 $config['last-opens'] = $config['last-opens'][-10, 10]
191 $xmldoc = Synchronizator.new(Document.new("<booh-gui-rc version='#{$VERSION}'/>"), $xmlaccesslock)
192 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
193 $config.each_pair { |key, value|
194 elem = $xmldoc.root.add_element key
196 $config[key].each_pair { |subkey, subvalue|
197 subelem = elem.add_element subkey
198 subelem.add_text subvalue.to_s
200 elsif value.is_a? Array
201 elem.add_text value.join('~~~')
206 elem.add_text value.to_s
210 ios = File.open($config_file, "w")
211 $xmldoc.write(ios, 0)
214 $tempfiles.each { |f|
221 def set_mousecursor(what, *widget)
222 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
223 if widget[0] && widget[0].window
224 widget[0].window.cursor = cursor
226 if $main_window && $main_window.window
227 $main_window.window.cursor = cursor
229 $current_cursor = what
231 def set_mousecursor_wait(*widget)
232 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
233 if Thread.current == Thread.main
234 Gtk.main_iteration while Gtk.events_pending?
237 def set_mousecursor_normal(*widget)
238 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
240 def push_mousecursor_wait(*widget)
241 if $current_cursor != Gdk::Cursor::WATCH
242 $save_cursor = $current_cursor
243 gtk_thread_protect { set_mousecursor_wait(*widget) }
246 def pop_mousecursor(*widget)
247 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
251 source = $xmldoc.root.attributes['source']
252 dest = $xmldoc.root.attributes['destination']
253 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
256 def full_src_dir_to_rel(path, source)
257 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
260 def build_full_dest_filename(filename)
261 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
264 def save_undo(name, closure, *params)
265 UndoHandler.save_undo(name, closure, [ *params ])
266 $undo_tb.sensitive = $undo_mb.sensitive = true
267 $redo_tb.sensitive = $redo_mb.sensitive = false
270 def view_element(filename, closures)
271 if entry2type(filename) == 'video'
272 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
278 w = create_window.set_title(filename)
280 msg 3, "filename: #{filename}"
281 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
282 #- typically this file won't exist in case of videos; try with the largest thumbnail around
283 if !File.exists?(dest_img)
284 if entry2type(filename) == 'video'
285 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
286 if not alternatives.empty?
287 dest_img = alternatives[-1]
290 push_mousecursor_wait
291 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
293 if !File.exists?(dest_img)
294 msg 2, _("Could not generate fullscreen thumbnail!")
299 aspect = utf8(_("Aspect: unknown"))
300 size = get_image_size(from_utf8("#{$current_path}/#{filename}"))
302 aspect = utf8(_("Aspect: %s") % sprintf("%1.3f", size[:x].to_f/size[:y]))
304 vbox = Gtk::VBox.new.add(Gtk::Image.new(dest_img)).add(Gtk::Label.new.set_markup("<i>#{aspect}</i>"))
305 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)))
306 evt.signal_connect('button-press-event') { |this, event|
307 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
308 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
310 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
312 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
313 delete_item.signal_connect('activate') {
315 closures[:delete].call(false)
318 menu.popup(nil, nil, event.button, event.time)
321 evt.signal_connect('button-release-event') { |this, event|
323 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
324 msg 3, "gesture delete: click-drag right button to the bottom"
326 closures[:delete].call(false)
327 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
331 tooltips = Gtk::Tooltips.new
332 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
334 w.signal_connect('key-press-event') { |w,event|
335 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
337 closures[:delete].call(false)
341 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
342 b.signal_connect('clicked') { w.destroy }
345 vb.pack_start(evt, false, false)
346 vb.pack_end(bottom, false, false)
349 w.signal_connect('delete-event') { w.destroy }
350 w.window_position = Gtk::Window::POS_CENTER
354 def scroll_upper(scrolledwindow, ypos_top)
355 newval = scrolledwindow.vadjustment.value -
356 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
357 if newval < scrolledwindow.vadjustment.lower
358 newval = scrolledwindow.vadjustment.lower
360 scrolledwindow.vadjustment.value = newval
363 def scroll_lower(scrolledwindow, ypos_bottom)
364 newval = scrolledwindow.vadjustment.value +
365 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
366 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
367 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
369 scrolledwindow.vadjustment.value = newval
372 def autoscroll_if_needed(scrolledwindow, image, textview)
373 #- autoscroll if cursor or image is not visible, if possible
374 if image && image.window || textview.window
375 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
376 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
377 current_miny_visible = scrolledwindow.vadjustment.value
378 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
379 if ypos_top < current_miny_visible
380 scroll_upper(scrolledwindow, ypos_top)
381 elsif ypos_bottom > current_maxy_visible
382 scroll_lower(scrolledwindow, ypos_bottom)
387 def create_editzone(scrolledwindow, pagenum, image)
388 frame = Gtk::Frame.new
389 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
390 frame.set_shadow_type(Gtk::SHADOW_IN)
391 textview.signal_connect('key-press-event') { |w, event|
392 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
393 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
394 scrolledwindow.signal_emit('key-press-event', event)
396 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
397 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
398 if event.keyval == Gdk::Keyval::GDK_Up
399 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
400 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
402 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
405 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
406 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
408 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
415 candidate_undo_text = nil
416 textview.signal_connect('focus-in-event') { |w, event|
417 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
418 candidate_undo_text = textview.buffer.text
422 textview.signal_connect('key-release-event') { |w, event|
423 if candidate_undo_text && candidate_undo_text != textview.buffer.text
425 save_undo(_("text edit"),
427 save_text = textview.buffer.text
428 textview.buffer.text = text
430 $notebook.set_page(pagenum)
432 textview.buffer.text = save_text
434 $notebook.set_page(pagenum)
436 }, candidate_undo_text)
437 candidate_undo_text = nil
440 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)
441 autoscroll_if_needed(scrolledwindow, image, textview)
446 return [ frame, textview ]
449 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
451 if !$modified_pixbufs[thumbnail_img]
452 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
453 elsif !$modified_pixbufs[thumbnail_img][:orig]
454 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
457 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
460 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
461 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
462 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
463 if pixbuf.height > desired_y
464 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
465 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
466 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
471 if $modified_pixbufs[thumbnail_img][:whitebalance]
472 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
475 #- fix gamma correction
476 if $modified_pixbufs[thumbnail_img][:gammacorrect]
477 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
480 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
483 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
486 #- update rotate attribute
487 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
488 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
490 #- change exif orientation if configured so (but forget in case of thumbnails caption)
491 if $config['rotate-set-exif'] == 'true' && xmlelem.attributes['filename']
492 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
495 $modified_pixbufs[thumbnail_img] ||= {}
496 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
497 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
499 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
502 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
505 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
507 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
509 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
510 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
512 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
513 $notebook.set_page(0)
514 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
519 def color_swap(xmldir, attributes_prefix)
521 if xmldir.attributes["#{attributes_prefix}color-swap"]
522 xmldir.delete_attribute("#{attributes_prefix}color-swap")
524 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
528 def enhance(xmldir, attributes_prefix)
530 if xmldir.attributes["#{attributes_prefix}enhance"]
531 xmldir.delete_attribute("#{attributes_prefix}enhance")
533 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
537 def change_seektime(xmldir, attributes_prefix, value)
539 xmldir.add_attribute("#{attributes_prefix}seektime", value)
542 def ask_new_seektime(xmldir, attributes_prefix)
544 value = xmldir.attributes["#{attributes_prefix}seektime"]
549 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
551 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
552 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
553 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
557 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
561 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
562 entry.signal_connect('key-press-event') { |w, event|
563 if event.keyval == Gdk::Keyval::GDK_Return
564 dialog.response(Gtk::Dialog::RESPONSE_OK)
566 elsif event.keyval == Gdk::Keyval::GDK_Escape
567 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
570 false #- propagate if needed
574 dialog.window_position = Gtk::Window::POS_MOUSE
577 dialog.run { |response|
580 if response == Gtk::Dialog::RESPONSE_OK
582 msg 3, "changing seektime to #{newval}"
583 return { :old => value, :new => newval }
590 def change_pano_amount(xmldir, attributes_prefix, value)
593 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
595 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
599 def ask_new_pano_amount(xmldir, attributes_prefix)
601 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
606 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
608 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
609 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
610 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
614 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
615 of this panorama image compared to other regular images. For example, if the panorama
616 was taken out of four photos on one row, counting the necessary overlap, the width of
617 this panorama image should probably be roughly three times the width of regular images.
619 With this information, booh will be able to generate panorama thumbnails looking
620 the right 'size', since the height of the thumbnail for this image will be similar
621 to the height of other thumbnails.
624 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)")))).
625 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
626 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
627 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
628 spin.signal_connect('value-changed') {
631 dialog.window_position = Gtk::Window::POS_MOUSE
634 spin.value = value.to_f
641 dialog.run { |response|
645 newval = spin.value.to_f
648 if response == Gtk::Dialog::RESPONSE_OK
650 msg 3, "changing panorama amount to #{newval}"
651 return { :old => value, :new => newval }
658 def change_whitebalance(xmlelem, attributes_prefix, value)
660 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
663 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
665 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
666 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
667 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
668 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
669 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
670 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
671 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
672 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
673 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
674 $modified_pixbufs[thumbnail_img] ||= {}
675 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
676 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
678 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
679 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
681 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
684 $modified_pixbufs[thumbnail_img] ||= {}
685 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
687 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
690 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
691 #- init $modified_pixbufs correctly
692 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
694 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
696 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
698 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
699 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
700 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
704 _("You can fix the <b>white balance</b> of the image, if your image is too blue
705 or too yellow because the recorder didn't detect the light correctly. Drag the
706 slider below the image to the left for more blue, to the right for more yellow.
710 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
712 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
714 dialog.window_position = Gtk::Window::POS_MOUSE
718 timeout = Gtk.timeout_add(100) {
719 if hs.value != lastval
722 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
728 dialog.run { |response|
729 Gtk.timeout_remove(timeout)
730 if response == Gtk::Dialog::RESPONSE_OK
732 newval = hs.value.to_s
733 msg 3, "changing white balance to #{newval}"
735 return { :old => value, :new => newval }
738 $modified_pixbufs[thumbnail_img] ||= {}
739 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
740 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
748 def change_gammacorrect(xmlelem, attributes_prefix, value)
750 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
753 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
755 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
756 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
757 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
758 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
759 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
760 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
761 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
762 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
763 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
764 $modified_pixbufs[thumbnail_img] ||= {}
765 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
766 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
768 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
769 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
771 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
774 $modified_pixbufs[thumbnail_img] ||= {}
775 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
777 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
780 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
781 #- init $modified_pixbufs correctly
782 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
784 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
786 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
788 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
789 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
790 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
794 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
795 or too bright. Drag the slider below the image.
799 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
801 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
803 dialog.window_position = Gtk::Window::POS_MOUSE
807 timeout = Gtk.timeout_add(100) {
808 if hs.value != lastval
811 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
817 dialog.run { |response|
818 Gtk.timeout_remove(timeout)
819 if response == Gtk::Dialog::RESPONSE_OK
821 newval = hs.value.to_s
822 msg 3, "gamma correction to #{newval}"
824 return { :old => value, :new => newval }
827 $modified_pixbufs[thumbnail_img] ||= {}
828 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
829 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
837 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
838 if File.exists?(destfile)
839 File.delete(destfile)
841 #- type can be 'element' or 'subdir'
843 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
845 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
849 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
851 push_mousecursor_wait
852 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
855 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
861 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
862 distribute_multiple_call = Proc.new { |action, arg|
863 $selected_elements.each_key { |path|
864 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
866 if possible_actions[:can_multiple] && $selected_elements.length > 0
867 UndoHandler.begin_batch
868 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
869 UndoHandler.end_batch
871 closures[action].call(arg)
873 $selected_elements = {}
876 if optionals.include?('change_image')
877 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
878 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
879 changeimg.signal_connect('activate') { closures[:change].call }
880 menu.append(Gtk::SeparatorMenuItem.new)
882 if !possible_actions[:can_multiple] || $selected_elements.length == 0
885 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
886 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
887 view.signal_connect('activate') { closures[:view].call }
889 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
890 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
891 view.signal_connect('activate') { closures[:view].call }
892 menu.append(Gtk::SeparatorMenuItem.new)
895 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
896 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
897 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
898 exif.signal_connect('activate') { show_popup($main_window,
899 utf8(`exif -m '#{fullpath}'`),
900 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
901 menu.append(Gtk::SeparatorMenuItem.new)
904 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
905 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
906 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
907 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
908 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
909 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
910 if !possible_actions[:can_multiple] || $selected_elements.length == 0
911 menu.append(Gtk::SeparatorMenuItem.new)
912 if !possible_actions[:forbid_left]
913 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
914 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
915 moveleft.signal_connect('activate') { closures[:move].call('left') }
916 if !possible_actions[:can_left]
917 moveleft.sensitive = false
920 if !possible_actions[:forbid_right]
921 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
922 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
923 moveright.signal_connect('activate') { closures[:move].call('right') }
924 if !possible_actions[:can_right]
925 moveright.sensitive = false
928 if optionals.include?('move_top')
929 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
930 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
931 movetop.signal_connect('activate') { closures[:move].call('top') }
932 if !possible_actions[:can_top]
933 movetop.sensitive = false
936 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
937 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
938 moveup.signal_connect('activate') { closures[:move].call('up') }
939 if !possible_actions[:can_up]
940 moveup.sensitive = false
942 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
943 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
944 movedown.signal_connect('activate') { closures[:move].call('down') }
945 if !possible_actions[:can_down]
946 movedown.sensitive = false
948 if optionals.include?('move_bottom')
949 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
950 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
951 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
952 if !possible_actions[:can_bottom]
953 movebottom.sensitive = false
958 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
959 menu.append(Gtk::SeparatorMenuItem.new)
960 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
961 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
962 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
963 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
964 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
965 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
966 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
967 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
968 seektime.signal_connect('activate') {
969 if possible_actions[:can_multiple] && $selected_elements.length > 0
970 if values = ask_new_seektime(nil, '')
971 distribute_multiple_call.call(:seektime, values)
974 closures[:seektime].call
979 menu.append( Gtk::SeparatorMenuItem.new)
980 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
981 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
982 gammacorrect.signal_connect('activate') {
983 if possible_actions[:can_multiple] && $selected_elements.length > 0
984 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
985 distribute_multiple_call.call(:gammacorrect, values)
988 closures[:gammacorrect].call
991 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
992 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
993 whitebalance.signal_connect('activate') {
994 if possible_actions[:can_multiple] && $selected_elements.length > 0
995 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
996 distribute_multiple_call.call(:whitebalance, values)
999 closures[:whitebalance].call
1002 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1003 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
1004 _("Enhance constrast"))))
1006 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
1008 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1009 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
1010 if type == 'image' && possible_actions[:can_panorama]
1011 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
1012 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
1013 panorama.signal_connect('activate') {
1014 if possible_actions[:can_multiple] && $selected_elements.length > 0
1015 if values = ask_new_pano_amount(nil, '')
1016 distribute_multiple_call.call(:pano, values)
1019 distribute_multiple_call.call(:pano)
1023 menu.append( Gtk::SeparatorMenuItem.new)
1024 if optionals.include?('delete')
1025 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1026 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1027 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1028 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1029 paste_item.signal_connect('activate') { closures[:paste].call }
1030 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1031 clear_item.signal_connect('activate') { $cuts = [] }
1033 paste_item.sensitive = clear_item.sensitive = false
1036 menu.append( Gtk::SeparatorMenuItem.new)
1038 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1039 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1040 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1041 editexternally.signal_connect('activate') {
1042 if check_image_editor
1043 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1049 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1050 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1051 if optionals.include?('delete')
1052 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1053 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1056 menu.popup(nil, nil, event.button, event.time)
1059 def delete_current_subalbum
1061 sel = $albums_tv.selection.selected_rows
1062 $xmldir.elements.each { |e|
1063 if e.name == 'image' || e.name == 'video'
1064 e.add_attribute('deleted', 'true')
1067 #- branch if we have a non deleted subalbum
1068 if $xmldir.child_byname_notattr('dir', 'deleted')
1069 $xmldir.delete_attribute('thumbnails-caption')
1070 $xmldir.delete_attribute('thumbnails-captionfile')
1072 $xmldir.add_attribute('deleted', 'true')
1074 while moveup.parent.name == 'dir'
1075 moveup = moveup.parent
1076 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1077 moveup.add_attribute('deleted', 'true')
1084 save_changes('forced')
1085 populate_subalbums_treeview(false)
1086 $albums_tv.selection.select_path(sel[0])
1092 $current_path = nil #- prevent save_changes from being rerun again
1093 sel = $albums_tv.selection.selected_rows
1094 restore_one = proc { |xmldir|
1095 xmldir.elements.each { |e|
1096 if e.name == 'dir' && e.attributes['deleted']
1099 e.delete_attribute('deleted')
1102 restore_one.call($xmldir)
1103 populate_subalbums_treeview(false)
1104 $albums_tv.selection.select_path(sel[0])
1107 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1110 frame1 = Gtk::Frame.new
1111 fullpath = from_utf8("#{$current_path}/#{filename}")
1113 my_gen_real_thumbnail = proc {
1114 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1118 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1119 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1120 pack_start(img = Gtk::Image.new).
1121 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1122 px, mask = pxb.render_pixmap_and_mask(0)
1123 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1124 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1126 frame1.add(img = Gtk::Image.new)
1129 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1130 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1131 my_gen_real_thumbnail.call
1133 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1136 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1138 tooltips = Gtk::Tooltips.new
1139 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1140 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1142 frame2, textview = create_editzone($autotable_sw, 1, img)
1143 textview.buffer.text = caption
1144 textview.set_justification(Gtk::Justification::CENTER)
1146 vbox = Gtk::VBox.new(false, 5)
1147 vbox.pack_start(evtbox, false, false)
1148 vbox.pack_start(frame2, false, false)
1149 autotable.append(vbox, filename)
1151 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1152 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1154 #- to be able to find widgets by name
1155 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1157 cleanup_all_thumbnails = proc {
1158 #- remove out of sync images
1159 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1160 for sizeobj in $images_size
1161 #- cannot use sizeobj because panoramic images will have a larger width
1162 Dir.glob("#{dest_img_base}-*.jpg") do |file|
1170 cleanup_all_thumbnails.call
1171 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1173 $xmldir.delete_attribute('already-generated')
1174 my_gen_real_thumbnail.call
1177 rotate_and_cleanup = proc { |angle|
1178 cleanup_all_thumbnails.call
1179 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1182 move = proc { |direction|
1183 do_method = "move_#{direction}"
1184 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1186 done = autotable.method(do_method).call(vbox)
1187 textview.grab_focus #- because if moving, focus is stolen
1191 save_undo(_("move %s") % direction,
1193 autotable.method(undo_method).call(vbox)
1194 textview.grab_focus #- because if moving, focus is stolen
1195 autoscroll_if_needed($autotable_sw, img, textview)
1196 $notebook.set_page(1)
1198 autotable.method(do_method).call(vbox)
1199 textview.grab_focus #- because if moving, focus is stolen
1200 autoscroll_if_needed($autotable_sw, img, textview)
1201 $notebook.set_page(1)
1207 color_swap_and_cleanup = proc {
1208 perform_color_swap_and_cleanup = proc {
1209 cleanup_all_thumbnails.call
1210 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1211 my_gen_real_thumbnail.call
1214 perform_color_swap_and_cleanup.call
1216 save_undo(_("color swap"),
1218 perform_color_swap_and_cleanup.call
1220 autoscroll_if_needed($autotable_sw, img, textview)
1221 $notebook.set_page(1)
1223 perform_color_swap_and_cleanup.call
1225 autoscroll_if_needed($autotable_sw, img, textview)
1226 $notebook.set_page(1)
1231 change_seektime_and_cleanup_real = proc { |values|
1232 perform_change_seektime_and_cleanup = proc { |val|
1233 cleanup_all_thumbnails.call
1234 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1235 my_gen_real_thumbnail.call
1237 perform_change_seektime_and_cleanup.call(values[:new])
1239 save_undo(_("specify seektime"),
1241 perform_change_seektime_and_cleanup.call(values[:old])
1243 autoscroll_if_needed($autotable_sw, img, textview)
1244 $notebook.set_page(1)
1246 perform_change_seektime_and_cleanup.call(values[:new])
1248 autoscroll_if_needed($autotable_sw, img, textview)
1249 $notebook.set_page(1)
1254 change_seektime_and_cleanup = proc {
1255 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1256 change_seektime_and_cleanup_real.call(values)
1260 change_pano_amount_and_cleanup_real = proc { |values|
1261 perform_change_pano_amount_and_cleanup = proc { |val|
1262 cleanup_all_thumbnails.call
1263 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1265 perform_change_pano_amount_and_cleanup.call(values[:new])
1267 save_undo(_("change panorama amount"),
1269 perform_change_pano_amount_and_cleanup.call(values[:old])
1271 autoscroll_if_needed($autotable_sw, img, textview)
1272 $notebook.set_page(1)
1274 perform_change_pano_amount_and_cleanup.call(values[:new])
1276 autoscroll_if_needed($autotable_sw, img, textview)
1277 $notebook.set_page(1)
1282 change_pano_amount_and_cleanup = proc {
1283 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1284 change_pano_amount_and_cleanup_real.call(values)
1288 whitebalance_and_cleanup_real = proc { |values|
1289 perform_change_whitebalance_and_cleanup = proc { |val|
1290 cleanup_all_thumbnails.call
1291 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1292 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1293 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1295 perform_change_whitebalance_and_cleanup.call(values[:new])
1297 save_undo(_("fix white balance"),
1299 perform_change_whitebalance_and_cleanup.call(values[:old])
1301 autoscroll_if_needed($autotable_sw, img, textview)
1302 $notebook.set_page(1)
1304 perform_change_whitebalance_and_cleanup.call(values[:new])
1306 autoscroll_if_needed($autotable_sw, img, textview)
1307 $notebook.set_page(1)
1312 whitebalance_and_cleanup = proc {
1313 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1314 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1315 whitebalance_and_cleanup_real.call(values)
1319 gammacorrect_and_cleanup_real = proc { |values|
1320 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1321 cleanup_all_thumbnails.call
1322 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1323 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1324 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1326 perform_change_gammacorrect_and_cleanup.call(values[:new])
1328 save_undo(_("gamma correction"),
1330 perform_change_gammacorrect_and_cleanup.call(values[:old])
1332 autoscroll_if_needed($autotable_sw, img, textview)
1333 $notebook.set_page(1)
1335 perform_change_gammacorrect_and_cleanup.call(values[:new])
1337 autoscroll_if_needed($autotable_sw, img, textview)
1338 $notebook.set_page(1)
1343 gammacorrect_and_cleanup = Proc.new {
1344 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1345 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1346 gammacorrect_and_cleanup_real.call(values)
1350 enhance_and_cleanup = proc {
1351 perform_enhance_and_cleanup = proc {
1352 cleanup_all_thumbnails.call
1353 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1354 my_gen_real_thumbnail.call
1357 cleanup_all_thumbnails.call
1358 perform_enhance_and_cleanup.call
1360 save_undo(_("enhance"),
1362 perform_enhance_and_cleanup.call
1364 autoscroll_if_needed($autotable_sw, img, textview)
1365 $notebook.set_page(1)
1367 perform_enhance_and_cleanup.call
1369 autoscroll_if_needed($autotable_sw, img, textview)
1370 $notebook.set_page(1)
1375 delete = proc { |isacut|
1376 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 })
1379 perform_delete = proc {
1380 after = autotable.get_next_widget(vbox)
1382 after = autotable.get_previous_widget(vbox)
1384 if $config['deleteondisk'] && !isacut
1385 msg 3, "scheduling for delete: #{fullpath}"
1386 $todelete << fullpath
1388 autotable.remove_widget(vbox)
1390 $vbox2widgets[after][:textview].grab_focus
1391 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1395 previous_pos = autotable.get_current_number(vbox)
1399 delete_current_subalbum
1401 save_undo(_("delete"),
1403 autotable.reinsert(pos, vbox, filename)
1404 $notebook.set_page(1)
1405 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1407 msg 3, "removing deletion schedule of: #{fullpath}"
1408 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1411 $notebook.set_page(1)
1420 $cuts << { :vbox => vbox, :filename => filename }
1421 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1426 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1429 autotable.queue_draws << proc {
1430 $vbox2widgets[last[:vbox]][:textview].grab_focus
1431 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1433 save_undo(_("paste"),
1435 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1436 $notebook.set_page(1)
1439 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1441 $notebook.set_page(1)
1444 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1449 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1450 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1451 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1452 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1454 textview.signal_connect('key-press-event') { |w, event|
1457 x, y = autotable.get_current_pos(vbox)
1458 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1459 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1460 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1461 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1463 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1464 $vbox2widgets[widget_up][:textview].grab_focus
1471 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1473 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1474 $vbox2widgets[widget_down][:textview].grab_focus
1481 if event.keyval == Gdk::Keyval::GDK_Left
1484 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1491 rotate_and_cleanup.call(-90)
1494 if event.keyval == Gdk::Keyval::GDK_Right
1495 next_ = autotable.get_next_widget(vbox)
1496 if next_ && autotable.get_current_pos(next_)[0] > x
1498 $vbox2widgets[next_][:textview].grab_focus
1505 rotate_and_cleanup.call(90)
1508 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1511 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1512 view_element(filename, { :delete => delete })
1515 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1518 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1522 !propagate #- propagate if needed
1525 $ignore_next_release = false
1526 evtbox.signal_connect('button-press-event') { |w, event|
1527 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1528 if event.state & Gdk::Window::BUTTON3_MASK != 0
1529 #- gesture redo: hold right mouse button then click left mouse button
1530 $config['nogestures'] or perform_redo
1531 $ignore_next_release = true
1533 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1535 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1537 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1538 elsif $enhance.active?
1539 enhance_and_cleanup.call
1540 elsif $delete.active?
1544 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1547 $button1_pressed_autotable = true
1548 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1549 if event.state & Gdk::Window::BUTTON1_MASK != 0
1550 #- gesture undo: hold left mouse button then click right mouse button
1551 $config['nogestures'] or perform_undo
1552 $ignore_next_release = true
1554 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1555 view_element(filename, { :delete => delete })
1560 evtbox.signal_connect('button-release-event') { |w, event|
1561 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1562 if !$ignore_next_release
1563 x, y = autotable.get_current_pos(vbox)
1564 next_ = autotable.get_next_widget(vbox)
1565 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1566 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1567 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1568 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1569 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1570 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1571 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1573 $ignore_next_release = false
1574 $gesture_press = nil
1579 #- handle reordering with drag and drop
1580 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1581 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1582 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1583 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1586 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1588 #- mouse gesture first (dnd disables button-release-event)
1589 if $gesture_press && $gesture_press[:filename] == filename
1590 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1591 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1592 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1593 rotate_and_cleanup.call(angle)
1594 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1596 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1597 msg 3, "gesture delete: click-drag right button to the bottom"
1599 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1604 ctxt.targets.each { |target|
1605 if target.name == 'reorder-elements'
1606 move_dnd = proc { |from,to|
1609 autotable.move(from, to)
1610 save_undo(_("reorder"),
1613 autotable.move(to - 1, from)
1615 autotable.move(to, from + 1)
1617 $notebook.set_page(1)
1619 autotable.move(from, to)
1620 $notebook.set_page(1)
1625 if $multiple_dnd.size == 0
1626 move_dnd.call(selection_data.data.to_i,
1627 autotable.get_current_number(vbox))
1629 UndoHandler.begin_batch
1630 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1632 #- need to update current position between each call
1633 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1634 autotable.get_current_number(vbox))
1636 UndoHandler.end_batch
1647 def create_auto_table
1649 $autotable = Gtk::AutoTable.new(5)
1651 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1652 thumbnails_vb = Gtk::VBox.new(false, 5)
1654 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1655 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1656 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1657 thumbnails_vb.add($autotable)
1659 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1660 $autotable_sw.add_with_viewport(thumbnails_vb)
1662 #- follows stuff for handling multiple elements selection
1663 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1665 update_selected = proc {
1666 $autotable.current_order.each { |path|
1667 w = $name2widgets[path][:evtbox].window
1668 xm = w.position[0] + w.size[0]/2
1669 ym = w.position[1] + w.size[1]/2
1670 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1671 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1672 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1673 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1676 if $selected_elements[path] && ! $selected_elements[path][:keep]
1677 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))
1678 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1679 $selected_elements.delete(path)
1684 $autotable.signal_connect('realize') { |w,e|
1685 gc = Gdk::GC.new($autotable.window)
1686 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1687 gc.function = Gdk::GC::INVERT
1688 #- autoscroll handling for DND and multiple selections
1689 Gtk.timeout_add(100) {
1690 if ! $autotable.window.nil?
1691 w, x, y, mask = $autotable.window.pointer
1692 if mask & Gdk::Window::BUTTON1_MASK != 0
1693 if y < $autotable_sw.vadjustment.value
1695 $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]])
1697 if $button1_pressed_autotable || press_x
1698 scroll_upper($autotable_sw, y)
1701 w, pos_x, pos_y = $autotable.window.pointer
1702 $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]])
1703 update_selected.call
1706 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1708 $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]])
1710 if $button1_pressed_autotable || press_x
1711 scroll_lower($autotable_sw, y)
1714 w, pos_x, pos_y = $autotable.window.pointer
1715 $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]])
1716 update_selected.call
1721 ! $autotable.window.nil?
1725 $autotable.signal_connect('button-press-event') { |w,e|
1727 if !$button1_pressed_autotable
1730 if e.state & Gdk::Window::SHIFT_MASK == 0
1731 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1732 $selected_elements = {}
1733 $statusbar.push(0, utf8(_("Nothing selected.")))
1735 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1737 set_mousecursor(Gdk::Cursor::TCROSS)
1741 $autotable.signal_connect('button-release-event') { |w,e|
1743 if $button1_pressed_autotable
1744 #- unselect all only now
1745 $multiple_dnd = $selected_elements.keys
1746 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1747 $selected_elements = {}
1748 $button1_pressed_autotable = false
1751 $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]])
1752 if $selected_elements.length > 0
1753 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1756 press_x = press_y = pos_x = pos_y = nil
1757 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1761 $autotable.signal_connect('motion-notify-event') { |w,e|
1764 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1768 $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]])
1769 update_selected.call
1775 def create_subalbums_page
1777 subalbums_hb = Gtk::HBox.new
1778 $subalbums_vb = Gtk::VBox.new(false, 5)
1779 subalbums_hb.pack_start($subalbums_vb, false, false)
1780 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1781 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1782 $subalbums_sw.add_with_viewport(subalbums_hb)
1785 def save_current_file
1791 ios = File.open($filename, "w")
1792 $xmldoc.write(ios, 0)
1794 rescue Iconv::IllegalSequence
1795 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1796 if ! ios.nil? && ! ios.closed?
1799 $xmldoc.xml_decl.encoding = 'UTF-8'
1800 ios = File.open($filename, "w")
1801 $xmldoc.write(ios, 0)
1812 def save_current_file_user
1813 save_tempfilename = $filename
1814 $filename = $orig_filename
1815 if ! save_current_file
1816 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1817 $filename = save_tempfilename
1821 $generated_outofline = false
1822 $filename = save_tempfilename
1824 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1825 $todelete.each { |f|
1830 def mark_document_as_dirty
1831 $xmldoc.elements.each('//dir') { |elem|
1832 elem.delete_attribute('already-generated')
1836 #- ret: true => ok false => cancel
1837 def ask_save_modifications(msg1, msg2, *options)
1839 options = options.size > 0 ? options[0] : {}
1841 if options[:disallow_cancel]
1842 dialog = Gtk::Dialog.new(msg1,
1844 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1845 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1846 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1848 dialog = Gtk::Dialog.new(msg1,
1850 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1851 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1852 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1853 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1855 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1856 dialog.vbox.add(Gtk::Label.new(msg2))
1857 dialog.window_position = Gtk::Window::POS_CENTER
1860 dialog.run { |response|
1862 if response == Gtk::Dialog::RESPONSE_YES
1863 if ! save_current_file_user
1864 return ask_save_modifications(msg1, msg2, options)
1867 #- if we have generated an album but won't save modifications, we must remove
1868 #- already-generated markers in original file
1869 if $generated_outofline
1871 $xmldoc = Synchronizator.new(REXML::Document.new(File.new($orig_filename)), $xmlaccesslock)
1872 mark_document_as_dirty
1873 ios = File.open($orig_filename, "w")
1874 $xmldoc.write(ios, 0)
1877 puts "exception: #{$!}"
1881 if response == Gtk::Dialog::RESPONSE_CANCEL
1884 $todelete = [] #- unconditionally clear the list of images/videos to delete
1890 def try_quit(*options)
1891 if ask_save_modifications(utf8(_("Save before quitting?")),
1892 utf8(_("Do you want to save your changes before quitting?")),
1898 def show_popup(parent, msg, *options)
1899 dialog = Gtk::Dialog.new
1900 if options[0] && options[0][:title]
1901 dialog.title = options[0][:title]
1903 dialog.title = utf8(_("Booh message"))
1905 lbl = Gtk::Label.new
1906 if options[0] && options[0][:nomarkup]
1911 if options[0] && options[0][:centered]
1912 lbl.set_justify(Gtk::Justification::CENTER)
1914 if options[0] && options[0][:selectable]
1915 lbl.selectable = true
1917 if options[0] && options[0][:topwidget]
1918 dialog.vbox.add(options[0][:topwidget])
1920 if options[0] && options[0][:scrolled]
1921 sw = Gtk::ScrolledWindow.new(nil, nil)
1922 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1923 sw.add_with_viewport(lbl)
1925 dialog.set_default_size(500, 600)
1927 dialog.vbox.add(lbl)
1928 dialog.set_default_size(200, 120)
1930 if options[0] && options[0][:okcancel]
1931 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1933 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1935 if options[0] && options[0][:pos_centered]
1936 dialog.window_position = Gtk::Window::POS_CENTER
1938 dialog.window_position = Gtk::Window::POS_MOUSE
1941 if options[0] && options[0][:linkurl]
1942 linkbut = Gtk::Button.new('')
1943 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1944 linkbut.signal_connect('clicked') {
1945 open_url(options[0][:linkurl])
1946 dialog.response(Gtk::Dialog::RESPONSE_OK)
1947 set_mousecursor_normal
1949 linkbut.relief = Gtk::RELIEF_NONE
1950 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1951 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1952 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1957 if !options[0] || !options[0][:not_transient]
1958 dialog.transient_for = parent
1959 dialog.run { |response|
1961 if options[0] && options[0][:okcancel]
1962 return response == Gtk::Dialog::RESPONSE_OK
1966 dialog.signal_connect('response') { dialog.destroy }
1970 def set_mainwindow_title(progress)
1971 filename = $orig_filename || $filename
1974 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
1976 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
1980 $main_window.title = 'booh - ' + File.basename(filename)
1982 $main_window.title = 'booh'
1987 def backend_wait_message(parent, msg, infopipe_path, mode)
1989 w.set_transient_for(parent)
1992 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1993 vb.pack_start(Gtk::Label.new(msg), false, false)
1995 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1996 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
1997 if mode != 'one dir scan'
1998 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2000 if mode == 'web-album'
2001 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2002 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2004 vb.pack_start(Gtk::HSeparator.new, false, false)
2006 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2007 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2008 vb.pack_end(bottom, false, false)
2011 update_progression_title_pb1 = proc {
2012 if mode == 'web-album'
2013 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2014 elsif mode != 'one dir scan'
2015 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2017 set_mainwindow_title(pb1_1.fraction)
2021 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2022 refresh_thread = Thread.new {
2023 directories_counter = 0
2024 while line = infopipe.gets
2025 if line =~ /^directories: (\d+), sizes: (\d+)/
2026 directories = $1.to_f + 1
2028 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2029 elements = $3.to_f + 1
2030 if mode == 'web-album'
2034 gtk_thread_protect { pb1_1.fraction = 0 }
2035 if mode != 'one dir scan'
2036 newtext = utf8(full_src_dir_to_rel($1, $2))
2037 newtext = '/' if newtext == ''
2038 gtk_thread_protect { pb1_2.text = newtext }
2039 directories_counter += 1
2040 gtk_thread_protect {
2041 pb1_2.fraction = directories_counter / directories
2042 update_progression_title_pb1.call
2045 elsif line =~ /^processing element$/
2046 element_counter += 1
2047 gtk_thread_protect {
2048 pb1_1.fraction = element_counter / elements
2049 update_progression_title_pb1.call
2051 elsif line =~ /^processing size$/
2052 element_counter += 1
2053 gtk_thread_protect {
2054 pb1_1.fraction = element_counter / elements
2055 update_progression_title_pb1.call
2057 elsif line =~ /^finished processing sizes$/
2058 gtk_thread_protect { pb1_1.fraction = 1 }
2059 elsif line =~ /^creating index.html$/
2060 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2061 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2062 directories_counter = 0
2063 elsif line =~ /^index.html: (.+)\|(.+)/
2064 newtext = utf8(full_src_dir_to_rel($1, $2))
2065 newtext = '/' if newtext == ''
2066 gtk_thread_protect { pb2.text = newtext }
2067 directories_counter += 1
2068 gtk_thread_protect {
2069 pb2.fraction = directories_counter / directories
2070 set_mainwindow_title(0.9 + pb2.fraction / 10)
2072 elsif line =~ /^die: (.*)$/
2079 w.signal_connect('delete-event') { w.destroy }
2080 w.signal_connect('destroy') {
2081 Thread.kill(refresh_thread)
2082 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2085 File.delete(infopipe_path)
2087 set_mainwindow_title(nil)
2089 w.window_position = Gtk::Window::POS_CENTER
2095 def call_backend(cmd, waitmsg, mode, params)
2096 pipe = Tempfile.new("boohpipe")
2097 Thread.critical = true
2100 system("mkfifo #{path}")
2101 Thread.critical = false
2102 cmd += " --info-pipe #{path}"
2103 button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2108 id, exitstatus = Process.waitpid2(pid)
2109 gtk_thread_protect { w8.destroy }
2111 if params[:successmsg]
2112 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2114 if params[:closure_after]
2115 gtk_thread_protect(¶ms[:closure_after])
2117 elsif exitstatus == 15
2118 #- say nothing, user aborted
2120 gtk_thread_protect { show_popup($main_window,
2121 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2127 button.signal_connect('clicked') {
2128 Process.kill('SIGTERM', pid)
2132 def save_changes(*forced)
2133 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2137 $xmldir.delete_attribute('already-generated')
2139 propagate_children = proc { |xmldir|
2140 if xmldir.attributes['subdirs-caption']
2141 xmldir.delete_attribute('already-generated')
2143 xmldir.elements.each('dir') { |element|
2144 propagate_children.call(element)
2148 if $xmldir.child_byname_notattr('dir', 'deleted')
2149 new_title = $subalbums_title.buffer.text
2150 if new_title != $xmldir.attributes['subdirs-caption']
2151 parent = $xmldir.parent
2152 if parent.name == 'dir'
2153 parent.delete_attribute('already-generated')
2155 propagate_children.call($xmldir)
2157 $xmldir.add_attribute('subdirs-caption', new_title)
2158 $xmldir.elements.each('dir') { |element|
2159 if !element.attributes['deleted']
2160 path = element.attributes['path']
2161 newtext = $subalbums_edits[path][:editzone].buffer.text
2162 if element.attributes['subdirs-caption']
2163 if element.attributes['subdirs-caption'] != newtext
2164 propagate_children.call(element)
2166 element.add_attribute('subdirs-caption', newtext)
2167 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2169 if element.attributes['thumbnails-caption'] != newtext
2170 element.delete_attribute('already-generated')
2172 element.add_attribute('thumbnails-caption', newtext)
2173 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2179 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2180 if $xmldir.attributes['thumbnails-caption']
2181 path = $xmldir.attributes['path']
2182 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2184 elsif $xmldir.attributes['thumbnails-caption']
2185 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2188 if $xmldir.attributes['thumbnails-caption']
2189 if edit = $subalbums_edits[$xmldir.attributes['path']]
2190 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2194 #- remove and reinsert elements to reflect new ordering
2197 $xmldir.elements.each { |element|
2198 if element.name == 'image' || element.name == 'video'
2199 saves[element.attributes['filename']] = element.remove
2203 $autotable.current_order.each { |path|
2204 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2205 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2208 saves.each_key { |path|
2209 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2210 chld.add_attribute('deleted', 'true')
2214 def sort_by_exif_date
2218 $xmldir.elements.each { |element|
2219 if element.name == 'image' || element.name == 'video'
2220 current_order << element.attributes['filename']
2224 #- look for EXIF dates
2227 if current_order.size > 20
2229 w.set_transient_for($main_window)
2231 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2232 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2233 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2234 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2235 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2236 vb.pack_end(bottom, false, false)
2238 w.signal_connect('delete-event') { w.destroy }
2239 w.window_position = Gtk::Window::POS_CENTER
2243 b.signal_connect('clicked') { aborted = true }
2245 current_order.each { |f|
2247 if entry2type(f) == 'image'
2249 pb.fraction = i.to_f / current_order.size
2250 Gtk.main_iteration while Gtk.events_pending?
2251 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2253 dates[f] = date_time
2266 current_order.each { |f|
2267 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2269 dates[f] = date_time
2275 $xmldir.elements.each { |element|
2276 if element.name == 'image' || element.name == 'video'
2277 saves[element.attributes['filename']] = element.remove
2281 neworder = smartsort(current_order, dates)
2284 $xmldir.add_element(saves[f].name, saves[f].attributes)
2287 #- let the auto-table reflect new ordering
2291 def remove_all_captions
2294 $autotable.current_order.each { |path|
2295 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2296 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2298 save_undo(_("remove all captions"),
2300 texts.each_key { |key|
2301 $name2widgets[key][:textview].buffer.text = texts[key]
2303 $notebook.set_page(1)
2305 texts.each_key { |key|
2306 $name2widgets[key][:textview].buffer.text = ''
2308 $notebook.set_page(1)
2314 $selected_elements.each_key { |path|
2315 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2321 $selected_elements = {}
2325 $undo_tb.sensitive = $undo_mb.sensitive = false
2326 $redo_tb.sensitive = $redo_mb.sensitive = false
2332 $subalbums_vb.children.each { |chld|
2333 $subalbums_vb.remove(chld)
2335 $subalbums = Gtk::Table.new(0, 0, true)
2336 current_y_sub_albums = 0
2338 $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"], $xmlaccesslock)
2339 $subalbums_edits = {}
2340 subalbums_counter = 0
2341 subalbums_edits_bypos = {}
2343 add_subalbum = proc { |xmldir, counter|
2344 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2345 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2346 if xmldir == $xmldir
2347 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2348 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2349 caption = xmldir.attributes['thumbnails-caption']
2350 infotype = 'thumbnails'
2352 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2353 captionfile, caption = find_subalbum_caption_info(xmldir)
2354 infotype = find_subalbum_info_type(xmldir)
2356 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2357 hbox = Gtk::HBox.new
2358 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2360 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2363 my_gen_real_thumbnail = proc {
2364 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2367 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2368 f.add(img = Gtk::Image.new)
2369 my_gen_real_thumbnail.call
2371 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2373 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2374 $subalbums.attach(hbox,
2375 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2377 frame, textview = create_editzone($subalbums_sw, 0, img)
2378 textview.buffer.text = caption
2379 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2380 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2382 change_image = proc {
2383 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2385 Gtk::FileChooser::ACTION_OPEN,
2387 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2388 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2389 fc.transient_for = $main_window
2390 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))
2391 f.add(preview_img = Gtk::Image.new)
2393 fc.signal_connect('update-preview') { |w|
2395 if fc.preview_filename
2396 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2397 fc.preview_widget_active = true
2399 rescue Gdk::PixbufError
2400 fc.preview_widget_active = false
2403 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2405 old_file = captionfile
2406 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2407 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2408 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2409 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2411 new_file = fc.filename
2412 msg 3, "new captionfile is: #{fc.filename}"
2413 perform_changefile = proc {
2414 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2415 $modified_pixbufs.delete(thumbnail_file)
2416 xmldir.delete_attribute("#{infotype}-rotate")
2417 xmldir.delete_attribute("#{infotype}-color-swap")
2418 xmldir.delete_attribute("#{infotype}-enhance")
2419 xmldir.delete_attribute("#{infotype}-seektime")
2420 my_gen_real_thumbnail.call
2422 perform_changefile.call
2424 save_undo(_("change caption file for sub-album"),
2426 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2427 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2428 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2429 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2430 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2431 my_gen_real_thumbnail.call
2432 $notebook.set_page(0)
2434 perform_changefile.call
2435 $notebook.set_page(0)
2443 if File.exists?(thumbnail_file)
2444 File.delete(thumbnail_file)
2446 my_gen_real_thumbnail.call
2449 rotate_and_cleanup = proc { |angle|
2450 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2451 if File.exists?(thumbnail_file)
2452 File.delete(thumbnail_file)
2456 move = proc { |direction|
2459 save_changes('forced')
2460 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2461 if direction == 'up'
2462 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2463 subalbums_edits_bypos[oldpos - 1][:position] += 1
2465 if direction == 'down'
2466 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2467 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2469 if direction == 'top'
2470 for i in 1 .. oldpos - 1
2471 subalbums_edits_bypos[i][:position] += 1
2473 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2475 if direction == 'bottom'
2476 for i in oldpos + 1 .. subalbums_counter
2477 subalbums_edits_bypos[i][:position] -= 1
2479 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2483 $xmldir.elements.each('dir') { |element|
2484 if (!element.attributes['deleted'])
2485 elems << [ element.attributes['path'], element.remove ]
2488 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2489 each { |e| $xmldir.add_element(e[1]) }
2490 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2491 $xmldir.elements.each('descendant::dir') { |elem|
2492 elem.delete_attribute('already-generated')
2495 sel = $albums_tv.selection.selected_rows
2497 populate_subalbums_treeview(false)
2498 $albums_tv.selection.select_path(sel[0])
2501 color_swap_and_cleanup = proc {
2502 perform_color_swap_and_cleanup = proc {
2503 color_swap(xmldir, "#{infotype}-")
2504 my_gen_real_thumbnail.call
2506 perform_color_swap_and_cleanup.call
2508 save_undo(_("color swap"),
2510 perform_color_swap_and_cleanup.call
2511 $notebook.set_page(0)
2513 perform_color_swap_and_cleanup.call
2514 $notebook.set_page(0)
2519 change_seektime_and_cleanup = proc {
2520 if values = ask_new_seektime(xmldir, "#{infotype}-")
2521 perform_change_seektime_and_cleanup = proc { |val|
2522 change_seektime(xmldir, "#{infotype}-", val)
2523 my_gen_real_thumbnail.call
2525 perform_change_seektime_and_cleanup.call(values[:new])
2527 save_undo(_("specify seektime"),
2529 perform_change_seektime_and_cleanup.call(values[:old])
2530 $notebook.set_page(0)
2532 perform_change_seektime_and_cleanup.call(values[:new])
2533 $notebook.set_page(0)
2539 whitebalance_and_cleanup = proc {
2540 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2541 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2542 perform_change_whitebalance_and_cleanup = proc { |val|
2543 change_whitebalance(xmldir, "#{infotype}-", val)
2544 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2545 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2546 if File.exists?(thumbnail_file)
2547 File.delete(thumbnail_file)
2550 perform_change_whitebalance_and_cleanup.call(values[:new])
2552 save_undo(_("fix white balance"),
2554 perform_change_whitebalance_and_cleanup.call(values[:old])
2555 $notebook.set_page(0)
2557 perform_change_whitebalance_and_cleanup.call(values[:new])
2558 $notebook.set_page(0)
2564 gammacorrect_and_cleanup = proc {
2565 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2566 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2567 perform_change_gammacorrect_and_cleanup = proc { |val|
2568 change_gammacorrect(xmldir, "#{infotype}-", val)
2569 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2570 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2571 if File.exists?(thumbnail_file)
2572 File.delete(thumbnail_file)
2575 perform_change_gammacorrect_and_cleanup.call(values[:new])
2577 save_undo(_("gamma correction"),
2579 perform_change_gammacorrect_and_cleanup.call(values[:old])
2580 $notebook.set_page(0)
2582 perform_change_gammacorrect_and_cleanup.call(values[:new])
2583 $notebook.set_page(0)
2589 enhance_and_cleanup = proc {
2590 perform_enhance_and_cleanup = proc {
2591 enhance(xmldir, "#{infotype}-")
2592 my_gen_real_thumbnail.call
2595 perform_enhance_and_cleanup.call
2597 save_undo(_("enhance"),
2599 perform_enhance_and_cleanup.call
2600 $notebook.set_page(0)
2602 perform_enhance_and_cleanup.call
2603 $notebook.set_page(0)
2608 evtbox.signal_connect('button-press-event') { |w, event|
2609 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2611 rotate_and_cleanup.call(90)
2613 rotate_and_cleanup.call(-90)
2614 elsif $enhance.active?
2615 enhance_and_cleanup.call
2618 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2619 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2620 { :forbid_left => true, :forbid_right => true,
2621 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2622 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2623 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2624 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2625 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2627 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2632 evtbox.signal_connect('button-press-event') { |w, event|
2633 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2637 evtbox.signal_connect('button-release-event') { |w, event|
2638 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2639 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2640 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2641 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2642 msg 3, "gesture rotate: #{angle}"
2643 rotate_and_cleanup.call(angle)
2646 $gesture_press = nil
2649 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2650 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2651 current_y_sub_albums += 1
2654 if $xmldir.child_byname_notattr('dir', 'deleted')
2656 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2657 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2658 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2659 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2660 #- this album image/caption
2661 if $xmldir.attributes['thumbnails-caption']
2662 add_subalbum.call($xmldir, 0)
2665 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2666 $xmldir.elements.each { |element|
2667 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2668 #- element (image or video) of this album
2669 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2670 msg 3, "dest_img: #{dest_img}"
2671 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2672 total[element.name] += 1
2674 if element.name == 'dir' && !element.attributes['deleted']
2675 #- sub-album image/caption
2676 add_subalbum.call(element, subalbums_counter += 1)
2677 total[element.name] += 1
2680 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2681 total['image'], total['video'], total['dir'] ]))
2682 $subalbums_vb.add($subalbums)
2683 $subalbums_vb.show_all
2685 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2686 $notebook.get_tab_label($autotable_sw).sensitive = false
2687 $notebook.set_page(0)
2688 $thumbnails_title.buffer.text = ''
2690 $notebook.get_tab_label($autotable_sw).sensitive = true
2691 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2694 if !$xmldir.child_byname_notattr('dir', 'deleted')
2695 $notebook.get_tab_label($subalbums_sw).sensitive = false
2696 $notebook.set_page(1)
2698 $notebook.get_tab_label($subalbums_sw).sensitive = true
2702 def pixbuf_or_nil(filename)
2704 return Gdk::Pixbuf.new(filename)
2710 def theme_choose(current)
2711 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2713 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2714 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2715 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2717 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2718 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2719 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2720 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2721 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2722 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2723 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2724 treeview.signal_connect('button-press-event') { |w, event|
2725 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2726 dialog.response(Gtk::Dialog::RESPONSE_OK)
2730 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2732 ([ $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|
2735 iter[0] = File.basename(dir)
2736 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2737 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2738 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2739 if File.basename(dir) == current
2740 treeview.selection.select_iter(iter)
2743 dialog.set_default_size(-1, 500)
2744 dialog.vbox.show_all
2746 dialog.run { |response|
2747 iter = treeview.selection.selected
2749 if response == Gtk::Dialog::RESPONSE_OK && iter
2750 return model.get_value(iter, 0)
2756 def show_password_protections
2757 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2758 child_iter = $albums_iters[xmldir.attributes['path']]
2759 if xmldir.attributes['password-protect']
2760 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2761 already_protected = true
2762 elsif already_protected
2763 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2765 pix = pix.saturate_and_pixelate(1, true)
2771 xmldir.elements.each('dir') { |elem|
2772 if !elem.attributes['deleted']
2773 examine_dir_elem.call(child_iter, elem, already_protected)
2777 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2780 def populate_subalbums_treeview(select_first)
2784 $subalbums_vb.children.each { |chld|
2785 $subalbums_vb.remove(chld)
2788 source = $xmldoc.root.attributes['source']
2789 msg 3, "source: #{source}"
2791 xmldir = $xmldoc.elements['//dir']
2792 if !xmldir || xmldir.attributes['path'] != source
2793 msg 1, _("Corrupted booh file...")
2797 append_dir_elem = proc { |parent_iter, xmldir|
2798 child_iter = $albums_ts.append(parent_iter)
2799 child_iter[0] = File.basename(xmldir.attributes['path'])
2800 child_iter[1] = xmldir.attributes['path']
2801 $albums_iters[xmldir.attributes['path']] = child_iter
2802 msg 3, "puttin location: #{xmldir.attributes['path']}"
2803 xmldir.elements.each('dir') { |elem|
2804 if !elem.attributes['deleted']
2805 append_dir_elem.call(child_iter, elem)
2809 append_dir_elem.call(nil, xmldir)
2810 show_password_protections
2812 $albums_tv.expand_all
2814 $albums_tv.selection.select_iter($albums_ts.iter_first)
2818 def select_current_theme
2819 select_theme($xmldoc.root.attributes['theme'],
2820 $xmldoc.root.attributes['limit-sizes'],
2821 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2822 $xmldoc.root.attributes['thumbnails-per-row'])
2825 def open_file(filename)
2829 $current_path = nil #- invalidate
2830 $modified_pixbufs = {}
2833 $subalbums_vb.children.each { |chld|
2834 $subalbums_vb.remove(chld)
2837 if !File.exists?(filename)
2838 return utf8(_("File not found."))
2842 $xmldoc = Synchronizator.new(REXML::Document.new(File.new(filename)), $xmlaccesslock)
2847 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2848 if entry2type(filename).nil?
2849 return utf8(_("Not a booh file!"))
2851 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."))
2855 if !source = $xmldoc.root.attributes['source']
2856 return utf8(_("Corrupted booh file..."))
2859 if !dest = $xmldoc.root.attributes['destination']
2860 return utf8(_("Corrupted booh file..."))
2863 if !theme = $xmldoc.root.attributes['theme']
2864 return utf8(_("Corrupted booh file..."))
2867 if $xmldoc.root.attributes['version'] < '0.9.0'
2868 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2869 mark_document_as_dirty
2870 if $xmldoc.root.attributes['version'] < '0.8.4'
2871 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2872 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2873 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2874 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2875 if old_dest_dir != new_dest_dir
2876 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2878 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2879 xmldir.elements.each { |element|
2880 if %w(image video).include?(element.name) && !element.attributes['deleted']
2881 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2882 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2883 Dir[old_name + '*'].each { |file|
2884 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2885 file != new_file and sys("mv '#{file}' '#{new_file}'")
2888 if element.name == 'dir' && !element.attributes['deleted']
2889 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2890 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2891 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2895 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2899 $xmldoc.root.add_attribute('version', $VERSION)
2902 select_current_theme
2904 $filename = filename
2905 set_mainwindow_title(nil)
2906 $default_size['thumbnails'] =~ /(.*)x(.*)/
2907 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2908 $albums_thumbnail_size =~ /(.*)x(.*)/
2909 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2911 populate_subalbums_treeview(true)
2913 $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
2917 def open_file_user(filename)
2918 result = open_file(filename)
2920 $config['last-opens'] ||= []
2921 if $config['last-opens'][-1] != utf8(filename)
2922 $config['last-opens'] << utf8(filename)
2924 $orig_filename = $filename
2925 $main_window.title = 'booh - ' + File.basename($orig_filename)
2926 tmp = Tempfile.new("boohtemp")
2927 Thread.critical = true
2928 $filename = tmp.path
2931 ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
2932 Thread.critical = false
2934 $tempfiles << $filename << "#{$filename}.backup"
2936 $orig_filename = nil
2942 if !ask_save_modifications(utf8(_("Save this album?")),
2943 utf8(_("Do you want to save the changes to this album?")),
2944 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2947 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2949 Gtk::FileChooser::ACTION_OPEN,
2951 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2952 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2953 fc.set_current_folder(File.expand_path("~/.booh"))
2954 fc.transient_for = $main_window
2957 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2958 push_mousecursor_wait(fc)
2959 msg = open_file_user(fc.filename)
2974 def additional_booh_options
2977 options += "--mproc #{$config['mproc'].to_i} "
2979 options += "--comments-format '#{$config['comments-format']}' "
2980 if $config['transcode-videos']
2981 options += "--transcode-videos '#{$config['transcode-videos']}' "
2986 def ask_multi_languages(value)
2988 spl = value.split(',')
2989 value = [ spl[0..-2], spl[-1] ]
2992 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
2995 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2996 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2998 lbl = Gtk::Label.new
3000 _("You can choose to activate <b>multi-languages</b> support for this web-album
3001 (it will work only if you publish your web-album on an Apache web-server). This will
3002 use the MultiViews feature of Apache; the pages will be served according to the
3003 value of the Accept-Language HTTP header sent by the web browsers, so that people
3004 with different languages preferences will be able to browse your web-album with
3005 navigation in their language (if language is available).
3008 dialog.vbox.add(lbl)
3009 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3010 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3011 add(languages = Gtk::Button.new))))
3013 pick_languages = proc {
3014 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3017 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3018 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3020 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3021 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3023 SUPPORTED_LANGUAGES.each { |lang|
3024 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3025 if ! value.nil? && value[0].include?(lang)
3031 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3032 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3033 fallback_language = nil
3034 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3035 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3036 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3037 fbl_rb.active = true
3038 fallback_language = SUPPORTED_LANGUAGES[0]
3040 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3041 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3042 rb.signal_connect('clicked') { fallback_language = lang }
3043 if ! value.nil? && value[1] == lang
3048 dialog2.window_position = Gtk::Window::POS_MOUSE
3052 dialog2.run { |response|
3054 if resp == Gtk::Dialog::RESPONSE_OK
3056 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3057 value[1] = fallback_language
3058 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3065 languages.signal_connect('clicked') {
3068 dialog.window_position = Gtk::Window::POS_MOUSE
3072 rb_yes.active = true
3073 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3075 rb_no.signal_connect('clicked') {
3079 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3092 dialog.run { |response|
3097 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3099 return [ true, nil ]
3101 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3110 if !ask_save_modifications(utf8(_("Save this album?")),
3111 utf8(_("Do you want to save the changes to this album?")),
3112 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3115 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3117 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3118 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3119 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3121 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3122 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3123 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3124 tbl.attach(src = Gtk::Entry.new,
3125 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3126 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3127 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3128 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3129 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3130 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3131 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3132 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3133 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3134 tbl.attach(dest = Gtk::Entry.new,
3135 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3136 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3137 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3138 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3139 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3140 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3141 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3142 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3143 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3145 tooltips = Gtk::Tooltips.new
3146 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3147 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3148 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3149 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3150 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3151 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3152 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)
3153 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3154 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3155 nperpage_model = Gtk::ListStore.new(String, String)
3156 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3157 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3158 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3159 nperpagecombo.set_attributes(crt, { :markup => 0 })
3160 iter = nperpage_model.append
3161 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3163 [ 12, 20, 30, 40, 50 ].each { |v|
3164 iter = nperpage_model.append
3165 iter[0] = iter[1] = v.to_s
3167 nperpagecombo.active = 0
3169 multilanguages_value = nil
3170 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3171 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3172 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)
3173 multilanguages.signal_connect('clicked') {
3174 retval = ask_multi_languages(multilanguages_value)
3176 multilanguages_value = retval[1]
3178 if multilanguages_value
3179 ml_label.text = utf8(_("Multi-languages: enabled."))
3181 ml_label.text = utf8(_("Multi-languages: disabled."))
3184 if $config['default-multi-languages']
3185 multilanguages_value = $config['default-multi-languages']
3186 ml_label.text = utf8(_("Multi-languages: enabled."))
3188 ml_label.text = utf8(_("Multi-languages: disabled."))
3191 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3192 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3193 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)
3194 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3195 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3196 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)
3198 src_nb_calculated_for = ''
3200 process_src_nb = proc {
3201 if src.text != src_nb_calculated_for
3202 src_nb_calculated_for = src.text
3204 Thread.kill(src_nb_thread)
3207 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3208 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3210 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3211 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3212 src_nb_thread = Thread.new {
3213 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3214 total = { 'image' => 0, 'video' => 0, nil => 0 }
3215 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3216 if File.basename(dir) =~ /^\./
3220 Dir.entries(dir.chomp).each { |file|
3221 total[entry2type(file)] += 1
3223 rescue Errno::EACCES, Errno::ENOENT
3227 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3231 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3234 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3240 timeout_src_nb = Gtk.timeout_add(100) {
3244 src_browse.signal_connect('clicked') {
3245 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3247 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3249 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3250 fc.transient_for = $main_window
3251 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3252 src.text = utf8(fc.filename)
3254 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3259 dest_browse.signal_connect('clicked') {
3260 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3262 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3264 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3265 fc.transient_for = $main_window
3266 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3267 dest.text = utf8(fc.filename)
3272 conf_browse.signal_connect('clicked') {
3273 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3275 Gtk::FileChooser::ACTION_SAVE,
3277 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3278 fc.transient_for = $main_window
3279 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3280 fc.set_current_folder(File.expand_path("~/.booh"))
3281 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3282 conf.text = utf8(fc.filename)
3289 recreate_theme_config = proc {
3290 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3292 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3293 $images_size.each { |s|
3294 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3298 tooltips.set_tip(cb, utf8(s['description']), nil)
3299 theme_sizes << { :widget => cb, :value => s['name'] }
3301 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3302 tooltips = Gtk::Tooltips.new
3303 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3304 theme_sizes << { :widget => cb, :value => 'original' }
3307 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3310 $allowed_N_values.each { |n|
3312 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3314 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3316 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3320 nperrows << { :widget => rb, :value => n }
3322 nperrowradios.show_all
3324 recreate_theme_config.call
3326 theme_button.signal_connect('clicked') {
3327 if newtheme = theme_choose(theme_button.label)
3328 theme_button.label = newtheme
3329 recreate_theme_config.call
3333 dialog.vbox.add(frame1)
3334 dialog.vbox.add(frame2)
3340 dialog.run { |response|
3341 if response == Gtk::Dialog::RESPONSE_OK
3342 srcdir = from_utf8_safe(src.text)
3343 destdir = from_utf8_safe(dest.text)
3344 confpath = from_utf8_safe(conf.text)
3345 if src.text != '' && srcdir == ''
3346 show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3348 elsif !File.directory?(srcdir)
3349 show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3351 elsif dest.text != '' && destdir == ''
3352 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3354 elsif destdir != make_dest_filename(destdir)
3355 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3357 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3358 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3359 inside it will be permanently removed before creating the web-album!
3360 Are you sure you want to continue?")), { :okcancel => true })
3362 elsif File.exists?(destdir) && !File.directory?(destdir)
3363 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3365 elsif conf.text == ''
3366 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3368 elsif conf.text != '' && confpath == ''
3369 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3371 elsif File.directory?(confpath)
3372 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3374 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3375 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3377 system("mkdir '#{destdir}'")
3378 if !File.directory?(destdir)
3379 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3391 srcdir = from_utf8(src.text)
3392 destdir = from_utf8(dest.text)
3393 configskel = File.expand_path(from_utf8(conf.text))
3394 theme = theme_button.label
3395 #- some sort of automatic theme preference
3396 $config['default-theme'] = theme
3397 $config['default-multi-languages'] = multilanguages_value
3398 $config['default-optimize32'] = optimize432.active?.to_s
3399 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3400 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3401 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3402 opt432 = optimize432.active?
3403 madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3404 indexlink = indexlinkentry.text.gsub('\'', ''')
3407 Thread.kill(src_nb_thread)
3408 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3411 Gtk.timeout_remove(timeout_src_nb)
3414 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3415 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3416 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3417 (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
3418 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3419 utf8(_("Please wait while scanning source directory...")),
3421 { :closure_after => proc {
3422 open_file_user(configskel)
3423 $main_window.urgency_hint = true
3429 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3431 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3432 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3433 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3435 source = $xmldoc.root.attributes['source']
3436 dest = $xmldoc.root.attributes['destination']
3437 theme = $xmldoc.root.attributes['theme']
3438 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3439 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3440 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3441 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3443 limit_sizes = limit_sizes.split(/,/)
3445 madewith = ($xmldoc.root.attributes['made-with'] || '').gsub(''', '\'')
3446 indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub(''', '\'')
3447 save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3449 tooltips = Gtk::Tooltips.new
3450 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3451 tbl.attach(Gtk::Label.new(utf8(_("Directory of source photos/videos: "))),
3452 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3453 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3454 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3455 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3456 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3457 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3458 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3459 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3460 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3461 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3462 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3464 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3465 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3466 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3467 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3468 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3469 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3470 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)
3471 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3472 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3473 nperpage_model = Gtk::ListStore.new(String, String)
3474 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3475 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3476 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3477 nperpagecombo.set_attributes(crt, { :markup => 0 })
3478 iter = nperpage_model.append
3479 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3481 [ 12, 20, 30, 40, 50 ].each { |v|
3482 iter = nperpage_model.append
3483 iter[0] = iter[1] = v.to_s
3484 if nperpage && nperpage == v.to_s
3485 nperpagecombo.active_iter = iter
3488 if nperpagecombo.active_iter.nil?
3489 nperpagecombo.active = 0
3492 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3493 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3494 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)
3496 if save_multilanguages_value
3497 ml_label.text = utf8(_("Multi-languages: enabled."))
3499 ml_label.text = utf8(_("Multi-languages: disabled."))
3503 multilanguages.signal_connect('clicked') {
3504 retval = ask_multi_languages(save_multilanguages_value)
3506 save_multilanguages_value = retval[1]