5 # A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
13 # Copyright (c) 2004-2008 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
27 require 'booh/libadds'
28 require 'booh/GtkAutoTable'
32 bindtextdomain("booh")
34 require 'booh/rexml/document'
37 require 'booh/booh-lib'
39 require 'booh/UndoHandler'
44 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
45 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
46 [ '--version', '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
49 #- default values for some globals
53 $ignore_videos = false
54 $button1_pressed_autotable = false
55 $generated_outofline = false
58 puts _("Usage: %s [OPTION]...") % File.basename($0)
60 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
65 parser = GetoptLong.new
66 parser.set_options(*$options.collect { |ary| ary[0..2] })
68 parser.each_option do |name, arg|
75 puts _("Booh version %s
77 Copyright (c) 2005-2008 Guillaume Cottenceau.
78 This is free software; see the source for copying conditions. There is NO
79 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
83 when '--verbose-level'
84 $verbose_level = arg.to_i
97 $config_file = File.expand_path('~/.booh-gui-rc')
98 if File.readable?($config_file)
99 xmldoc = REXML::Document.new(File.new($config_file))
100 xmldoc.root.elements.each { |element|
101 txt = element.get_text
103 if txt.value =~ /~~~/ || element.name == 'last-opens'
104 $config[element.name] = txt.value.split(/~~~/)
106 $config[element.name] = txt.value
108 elsif element.elements.size == 0
109 $config[element.name] = ''
111 $config[element.name] = {}
112 element.each { |chld|
114 $config[element.name][chld.name] = txt ? txt.value : nil
119 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
120 $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
121 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
122 $config['comments-format'] ||= '%t'
123 if !FileTest.directory?(File.expand_path('~/.booh'))
124 system("mkdir ~/.booh")
126 if $config['mproc'].nil?
128 for line in IO.readlines('/proc/cpuinfo') do
129 line =~ /^processor/ and cpus += 1
132 $config['mproc'] = cpus
135 $config['rotate-set-exif'] ||= 'true'
141 if !system("which convert >/dev/null 2>/dev/null")
142 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
143 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
146 if !system("which identify >/dev/null 2>/dev/null")
147 show_popup($main_window, utf8(_("The program 'identify' is needed to get photos sizes and EXIF data. Please install it.
148 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
150 if !system("which exif >/dev/null 2>/dev/null")
151 show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
153 missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
155 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
158 viewer_binary = $config['video-viewer'].split.first
159 if viewer_binary && !File.executable?(viewer_binary)
160 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
161 You should fix this in Edit/Preferences so that you can view videos.
163 Problem was: '%s' is not an executable file.
164 Hint: don't forget to specify the full path to the executable,
165 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
169 def check_image_editor
170 image_editor_binary = $config['image-editor'].split.first
171 if image_editor_binary && !File.executable?(image_editor_binary)
172 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
173 You should fix this in Edit/Preferences so that you can edit photos externally.
175 Problem was: '%s' is not an executable file.
176 Hint: don't forget to specify the full path to the executable,
177 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
185 if $config['last-opens'] && $config['last-opens'].size > 10
186 $config['last-opens'] = $config['last-opens'][-10, 10]
189 xmldoc = Document.new("<booh-gui-rc version='#{$VERSION}'/>")
190 xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
191 $config.each_pair { |key, value|
192 elem = xmldoc.root.add_element key
194 $config[key].each_pair { |subkey, subvalue|
195 subelem = elem.add_element subkey
196 subelem.add_text subvalue.to_s
198 elsif value.is_a? Array
199 elem.add_text value.join('~~~')
204 elem.add_text value.to_s
208 ios = File.open($config_file, "w")
212 $tempfiles.each { |f|
219 def set_mousecursor(what, *widget)
220 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
221 if widget[0] && widget[0].window
222 widget[0].window.cursor = cursor
224 if $main_window && $main_window.window
225 $main_window.window.cursor = cursor
227 $current_cursor = what
229 def set_mousecursor_wait(*widget)
230 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
231 if Thread.current == Thread.main
232 Gtk.main_iteration while Gtk.events_pending?
235 def set_mousecursor_normal(*widget)
236 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
238 def push_mousecursor_wait(*widget)
239 if $current_cursor != Gdk::Cursor::WATCH
240 $save_cursor = $current_cursor
241 gtk_thread_protect { set_mousecursor_wait(*widget) }
244 def pop_mousecursor(*widget)
245 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
249 source = $xmldoc.root.attributes['source']
250 dest = $xmldoc.root.attributes['destination']
251 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
254 def full_src_dir_to_rel(path, source)
255 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
258 def build_full_dest_filename(filename)
259 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
262 def save_undo(name, closure, *params)
263 UndoHandler.save_undo(name, closure, [ *params ])
264 $undo_tb.sensitive = $undo_mb.sensitive = true
265 $redo_tb.sensitive = $redo_mb.sensitive = false
268 def view_element(filename, closures)
269 if entry2type(filename) == 'video'
270 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
276 w = create_window.set_title(filename)
278 msg 3, "filename: #{filename}"
279 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
280 #- typically this file won't exist in case of videos; try with the largest thumbnail around
281 if !File.exists?(dest_img)
282 if entry2type(filename) == 'video'
283 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
284 if not alternatives.empty?
285 dest_img = alternatives[-1]
288 push_mousecursor_wait
289 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
291 if !File.exists?(dest_img)
292 msg 2, _("Could not generate fullscreen thumbnail!")
297 aspect = utf8(_("Aspect: unknown"))
298 size = get_image_size(from_utf8("#{$current_path}/#{filename}"))
300 aspect = utf8(_("Aspect: %s") % sprintf("%1.3f", size[:x].to_f/size[:y]))
302 vbox = Gtk::VBox.new.add(Gtk::Image.new(dest_img)).add(Gtk::Label.new.set_markup("<i>#{aspect}</i>"))
303 evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(vbox).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
304 evt.signal_connect('button-press-event') { |this, event|
305 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
306 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
308 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
310 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
311 delete_item.signal_connect('activate') {
313 closures[:delete].call(false)
316 menu.popup(nil, nil, event.button, event.time)
319 evt.signal_connect('button-release-event') { |this, event|
321 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
322 msg 3, "gesture delete: click-drag right button to the bottom"
324 closures[:delete].call(false)
325 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
329 tooltips = Gtk::Tooltips.new
330 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
332 w.signal_connect('key-press-event') { |w,event|
333 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
335 closures[:delete].call(false)
339 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
340 b.signal_connect('clicked') { w.destroy }
343 vb.pack_start(evt, false, false)
344 vb.pack_end(bottom, false, false)
347 w.signal_connect('delete-event') { w.destroy }
348 w.window_position = Gtk::Window::POS_CENTER
352 def scroll_upper(scrolledwindow, ypos_top)
353 newval = scrolledwindow.vadjustment.value -
354 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
355 if newval < scrolledwindow.vadjustment.lower
356 newval = scrolledwindow.vadjustment.lower
358 scrolledwindow.vadjustment.value = newval
361 def scroll_lower(scrolledwindow, ypos_bottom)
362 newval = scrolledwindow.vadjustment.value +
363 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
364 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
365 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
367 scrolledwindow.vadjustment.value = newval
370 def autoscroll_if_needed(scrolledwindow, image, textview)
371 #- autoscroll if cursor or image is not visible, if possible
372 if image && image.window || textview.window
373 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
374 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
375 current_miny_visible = scrolledwindow.vadjustment.value
376 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
377 if ypos_top < current_miny_visible
378 scroll_upper(scrolledwindow, ypos_top)
379 elsif ypos_bottom > current_maxy_visible
380 scroll_lower(scrolledwindow, ypos_bottom)
385 def create_editzone(scrolledwindow, pagenum, image)
386 frame = Gtk::Frame.new
387 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
388 frame.set_shadow_type(Gtk::SHADOW_IN)
389 textview.signal_connect('key-press-event') { |w, event|
390 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
391 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
392 scrolledwindow.signal_emit('key-press-event', event)
394 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
395 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
396 if event.keyval == Gdk::Keyval::GDK_Up
397 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
398 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
400 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
403 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
404 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
406 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
413 candidate_undo_text = nil
414 textview.signal_connect('focus-in-event') { |w, event|
415 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
416 candidate_undo_text = textview.buffer.text
420 textview.signal_connect('key-release-event') { |w, event|
421 if candidate_undo_text && candidate_undo_text != textview.buffer.text
423 save_undo(_("text edit"),
425 save_text = textview.buffer.text
426 textview.buffer.text = text
428 $notebook.set_page(pagenum)
430 textview.buffer.text = save_text
432 $notebook.set_page(pagenum)
434 }, candidate_undo_text)
435 candidate_undo_text = nil
438 if event.state != 0 || ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
439 autoscroll_if_needed(scrolledwindow, image, textview)
444 return [ frame, textview ]
447 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
449 if !$modified_pixbufs[thumbnail_img]
450 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
451 elsif !$modified_pixbufs[thumbnail_img][:orig]
452 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
455 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
458 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
459 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
460 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
461 if pixbuf.height > desired_y
462 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
463 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
464 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
469 if $modified_pixbufs[thumbnail_img][:whitebalance]
470 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
473 #- fix gamma correction
474 if $modified_pixbufs[thumbnail_img][:gammacorrect]
475 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
478 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
481 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
484 #- update rotate attribute
485 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
486 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
488 #- change exif orientation if configured so (but forget in case of thumbnails caption)
489 if $config['rotate-set-exif'] == 'true' && xmlelem.attributes['filename']
490 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
493 $modified_pixbufs[thumbnail_img] ||= {}
494 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
495 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
497 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
500 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
503 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
505 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
507 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
508 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
510 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
511 $notebook.set_page(0)
512 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
517 def color_swap(xmldir, attributes_prefix)
519 rexml_thread_protect {
520 if xmldir.attributes["#{attributes_prefix}color-swap"]
521 xmldir.delete_attribute("#{attributes_prefix}color-swap")
523 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
528 def enhance(xmldir, attributes_prefix)
530 rexml_thread_protect {
531 if xmldir.attributes["#{attributes_prefix}enhance"]
532 xmldir.delete_attribute("#{attributes_prefix}enhance")
534 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
539 def change_seektime(xmldir, attributes_prefix, value)
541 rexml_thread_protect {
542 xmldir.add_attribute("#{attributes_prefix}seektime", value)
546 def ask_new_seektime(xmldir, attributes_prefix)
547 rexml_thread_protect {
549 value = xmldir.attributes["#{attributes_prefix}seektime"]
555 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
557 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
558 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
559 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
563 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
567 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
568 entry.signal_connect('key-press-event') { |w, event|
569 if event.keyval == Gdk::Keyval::GDK_Return
570 dialog.response(Gtk::Dialog::RESPONSE_OK)
572 elsif event.keyval == Gdk::Keyval::GDK_Escape
573 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
576 false #- propagate if needed
580 dialog.window_position = Gtk::Window::POS_MOUSE
583 dialog.run { |response|
586 if response == Gtk::Dialog::RESPONSE_OK
588 msg 3, "changing seektime to #{newval}"
589 return { :old => value, :new => newval }
596 def change_pano_amount(xmldir, attributes_prefix, value)
598 rexml_thread_protect {
600 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
602 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
607 def ask_new_pano_amount(xmldir, attributes_prefix)
608 rexml_thread_protect {
610 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
616 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
618 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
619 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
620 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
624 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
625 of this panorama image compared to other regular images. For example, if the panorama
626 was taken out of four photos on one row, counting the necessary overlap, the width of
627 this panorama image should probably be roughly three times the width of regular images.
629 With this information, booh will be able to generate panorama thumbnails looking
630 the right 'size', since the height of the thumbnail for this image will be similar
631 to the height of other thumbnails.
634 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("none (not a panorama image)")))).
635 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
636 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
637 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
638 spin.signal_connect('value-changed') {
641 dialog.window_position = Gtk::Window::POS_MOUSE
644 spin.value = value.to_f
651 dialog.run { |response|
655 newval = spin.value.to_f
658 if response == Gtk::Dialog::RESPONSE_OK
660 msg 3, "changing panorama amount to #{newval}"
661 return { :old => value, :new => newval }
668 def change_whitebalance(xmlelem, attributes_prefix, value)
670 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
673 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
675 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
676 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
677 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
678 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
679 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
680 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
681 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
682 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
683 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
684 $modified_pixbufs[thumbnail_img] ||= {}
685 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
686 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
688 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
689 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
691 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
694 $modified_pixbufs[thumbnail_img] ||= {}
695 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
697 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
700 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
701 #- init $modified_pixbufs correctly
702 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
704 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
706 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
708 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
709 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
710 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
714 _("You can fix the <b>white balance</b> of the image, if your image is too blue
715 or too yellow because the recorder didn't detect the light correctly. Drag the
716 slider below the image to the left for more blue, to the right for more yellow.
720 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
722 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
724 dialog.window_position = Gtk::Window::POS_MOUSE
728 timeout = Gtk.timeout_add(100) {
729 if hs.value != lastval
732 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
738 dialog.run { |response|
739 Gtk.timeout_remove(timeout)
740 if response == Gtk::Dialog::RESPONSE_OK
742 newval = hs.value.to_s
743 msg 3, "changing white balance to #{newval}"
745 return { :old => value, :new => newval }
748 $modified_pixbufs[thumbnail_img] ||= {}
749 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
750 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
758 def change_gammacorrect(xmlelem, attributes_prefix, value)
760 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
763 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
765 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
766 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
767 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
768 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
769 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
770 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
771 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
772 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
773 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
774 $modified_pixbufs[thumbnail_img] ||= {}
775 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
776 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
778 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
779 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
781 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
784 $modified_pixbufs[thumbnail_img] ||= {}
785 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
787 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
790 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
791 #- init $modified_pixbufs correctly
792 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
794 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
796 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
798 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
799 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
800 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
804 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
805 or too bright. Drag the slider below the image.
809 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
811 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
813 dialog.window_position = Gtk::Window::POS_MOUSE
817 timeout = Gtk.timeout_add(100) {
818 if hs.value != lastval
821 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
827 dialog.run { |response|
828 Gtk.timeout_remove(timeout)
829 if response == Gtk::Dialog::RESPONSE_OK
831 newval = hs.value.to_s
832 msg 3, "gamma correction to #{newval}"
834 return { :old => value, :new => newval }
837 $modified_pixbufs[thumbnail_img] ||= {}
838 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
839 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
847 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
848 if File.exists?(destfile)
849 File.delete(destfile)
851 #- type can be 'element' or 'subdir'
853 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
855 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
859 $max_gen_thumbnail_threads = nil
860 $current_gen_thumbnail_threads = 0
861 $gen_thumbnail_monitor = Monitor.new
863 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
864 if $max_gen_thumbnail_threads.nil?
865 $max_gen_thumbnail_threads = 1 + $config['mproc'].to_i || 1
868 push_mousecursor_wait
869 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
872 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
877 $gen_thumbnail_monitor.synchronize {
878 if $current_gen_thumbnail_threads < $max_gen_thumbnail_threads
879 $current_gen_thumbnail_threads += 1
884 msg 3, "generate thumbnail from new thread"
887 $gen_thumbnail_monitor.synchronize {
888 $current_gen_thumbnail_threads -= 1
892 msg 3, "generate thumbnail from current thread"
897 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
898 distribute_multiple_call = Proc.new { |action, arg|
899 $selected_elements.each_key { |path|
900 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
902 if possible_actions[:can_multiple] && $selected_elements.length > 0
903 UndoHandler.begin_batch
904 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
905 UndoHandler.end_batch
907 closures[action].call(arg)
909 $selected_elements = {}
912 if optionals.include?('change_image')
913 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
914 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
915 changeimg.signal_connect('activate') { closures[:change].call }
916 menu.append(Gtk::SeparatorMenuItem.new)
918 if !possible_actions[:can_multiple] || $selected_elements.length == 0
921 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
922 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
923 view.signal_connect('activate') { closures[:view].call }
925 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
926 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
927 view.signal_connect('activate') { closures[:view].call }
928 menu.append(Gtk::SeparatorMenuItem.new)
931 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
932 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
933 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
934 exif.signal_connect('activate') { show_popup($main_window,
935 utf8(`exif -m '#{fullpath}'`),
936 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
937 menu.append(Gtk::SeparatorMenuItem.new)
940 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
941 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
942 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
943 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
944 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
945 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
946 if !possible_actions[:can_multiple] || $selected_elements.length == 0
947 menu.append(Gtk::SeparatorMenuItem.new)
948 if !possible_actions[:forbid_left]
949 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
950 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
951 moveleft.signal_connect('activate') { closures[:move].call('left') }
952 if !possible_actions[:can_left]
953 moveleft.sensitive = false
956 if !possible_actions[:forbid_right]
957 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
958 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
959 moveright.signal_connect('activate') { closures[:move].call('right') }
960 if !possible_actions[:can_right]
961 moveright.sensitive = false
964 if optionals.include?('move_top')
965 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
966 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
967 movetop.signal_connect('activate') { closures[:move].call('top') }
968 if !possible_actions[:can_top]
969 movetop.sensitive = false
972 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
973 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
974 moveup.signal_connect('activate') { closures[:move].call('up') }
975 if !possible_actions[:can_up]
976 moveup.sensitive = false
978 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
979 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
980 movedown.signal_connect('activate') { closures[:move].call('down') }
981 if !possible_actions[:can_down]
982 movedown.sensitive = false
984 if optionals.include?('move_bottom')
985 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
986 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
987 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
988 if !possible_actions[:can_bottom]
989 movebottom.sensitive = false
994 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
995 menu.append(Gtk::SeparatorMenuItem.new)
996 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
997 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
998 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
999 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
1000 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
1001 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
1002 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
1003 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
1004 seektime.signal_connect('activate') {
1005 if possible_actions[:can_multiple] && $selected_elements.length > 0
1006 if values = ask_new_seektime(nil, '')
1007 distribute_multiple_call.call(:seektime, values)
1010 closures[:seektime].call
1015 menu.append( Gtk::SeparatorMenuItem.new)
1016 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
1017 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
1018 gammacorrect.signal_connect('activate') {
1019 if possible_actions[:can_multiple] && $selected_elements.length > 0
1020 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
1021 distribute_multiple_call.call(:gammacorrect, values)
1024 closures[:gammacorrect].call
1027 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
1028 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
1029 whitebalance.signal_connect('activate') {
1030 if possible_actions[:can_multiple] && $selected_elements.length > 0
1031 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
1032 distribute_multiple_call.call(:whitebalance, values)
1035 closures[:whitebalance].call
1038 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1039 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(rexml_thread_protect { xmldir.attributes["#{attributes_prefix}enhance"] } ? _("Original contrast") :
1040 _("Enhance constrast"))))
1042 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
1044 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1045 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
1046 if type == 'image' && possible_actions[:can_panorama]
1047 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
1048 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
1049 panorama.signal_connect('activate') {
1050 if possible_actions[:can_multiple] && $selected_elements.length > 0
1051 if values = ask_new_pano_amount(nil, '')
1052 distribute_multiple_call.call(:pano, values)
1055 distribute_multiple_call.call(:pano)
1059 menu.append( Gtk::SeparatorMenuItem.new)
1060 if optionals.include?('delete')
1061 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1062 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1063 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1064 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1065 paste_item.signal_connect('activate') { closures[:paste].call }
1066 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1067 clear_item.signal_connect('activate') { $cuts = [] }
1069 paste_item.sensitive = clear_item.sensitive = false
1072 menu.append( Gtk::SeparatorMenuItem.new)
1074 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1075 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1076 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1077 editexternally.signal_connect('activate') {
1078 if check_image_editor
1079 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1085 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1086 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1087 if optionals.include?('delete')
1088 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1089 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1092 menu.popup(nil, nil, event.button, event.time)
1095 def delete_current_subalbum
1097 sel = $albums_tv.selection.selected_rows
1098 $xmldir.elements.each { |e|
1099 if e.name == 'image' || e.name == 'video'
1100 e.add_attribute('deleted', 'true')
1103 #- branch if we have a non deleted subalbum
1104 if $xmldir.child_byname_notattr('dir', 'deleted')
1105 $xmldir.delete_attribute('thumbnails-caption')
1106 $xmldir.delete_attribute('thumbnails-captionfile')
1108 $xmldir.add_attribute('deleted', 'true')
1110 while moveup.parent.name == 'dir'
1111 moveup = moveup.parent
1112 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1113 moveup.add_attribute('deleted', 'true')
1120 save_changes('forced')
1121 populate_subalbums_treeview(false)
1122 $albums_tv.selection.select_path(sel[0])
1128 $current_path = nil #- prevent save_changes from being rerun again
1129 sel = $albums_tv.selection.selected_rows
1130 restore_one = proc { |xmldir|
1131 xmldir.elements.each { |e|
1132 if e.name == 'dir' && e.attributes['deleted']
1135 e.delete_attribute('deleted')
1138 restore_one.call($xmldir)
1139 populate_subalbums_treeview(false)
1140 $albums_tv.selection.select_path(sel[0])
1143 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1146 frame1 = Gtk::Frame.new
1147 fullpath = from_utf8("#{$current_path}/#{filename}")
1149 my_gen_real_thumbnail = proc {
1150 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1154 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1155 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1156 pack_start(img = Gtk::Image.new).
1157 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1158 px, mask = pxb.render_pixmap_and_mask(0)
1159 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1160 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1162 frame1.add(img = Gtk::Image.new)
1165 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1166 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1167 my_gen_real_thumbnail.call
1169 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1172 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1174 tooltips = Gtk::Tooltips.new
1175 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1176 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1178 frame2, textview = create_editzone($autotable_sw, 1, img)
1179 textview.buffer.text = caption
1180 textview.set_justification(Gtk::Justification::CENTER)
1182 vbox = Gtk::VBox.new(false, 5)
1183 vbox.pack_start(evtbox, false, false)
1184 vbox.pack_start(frame2, false, false)
1185 autotable.append(vbox, filename)
1187 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1188 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1190 #- to be able to find widgets by name
1191 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1193 cleanup_all_thumbnails = proc {
1194 #- remove out of sync images
1195 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1196 for sizeobj in $images_size
1197 #- cannot use sizeobj because panoramic images will have a larger width
1198 Dir.glob("#{dest_img_base}-*.jpg") do |file|
1206 cleanup_all_thumbnails.call
1207 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1209 rexml_thread_protect {
1210 $xmldir.delete_attribute('already-generated')
1212 my_gen_real_thumbnail.call
1215 rotate_and_cleanup = proc { |angle|
1216 cleanup_all_thumbnails.call
1217 rexml_thread_protect {
1218 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1222 move = proc { |direction|
1223 do_method = "move_#{direction}"
1224 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1226 done = autotable.method(do_method).call(vbox)
1227 textview.grab_focus #- because if moving, focus is stolen
1231 save_undo(_("move %s") % direction,
1233 autotable.method(undo_method).call(vbox)
1234 textview.grab_focus #- because if moving, focus is stolen
1235 autoscroll_if_needed($autotable_sw, img, textview)
1236 $notebook.set_page(1)
1238 autotable.method(do_method).call(vbox)
1239 textview.grab_focus #- because if moving, focus is stolen
1240 autoscroll_if_needed($autotable_sw, img, textview)
1241 $notebook.set_page(1)
1247 color_swap_and_cleanup = proc {
1248 perform_color_swap_and_cleanup = proc {
1249 cleanup_all_thumbnails.call
1250 rexml_thread_protect {
1251 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1253 my_gen_real_thumbnail.call
1256 perform_color_swap_and_cleanup.call
1258 save_undo(_("color swap"),
1260 perform_color_swap_and_cleanup.call
1262 autoscroll_if_needed($autotable_sw, img, textview)
1263 $notebook.set_page(1)
1265 perform_color_swap_and_cleanup.call
1267 autoscroll_if_needed($autotable_sw, img, textview)
1268 $notebook.set_page(1)
1273 change_seektime_and_cleanup_real = proc { |values|
1274 perform_change_seektime_and_cleanup = proc { |val|
1275 cleanup_all_thumbnails.call
1276 rexml_thread_protect {
1277 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1279 my_gen_real_thumbnail.call
1281 perform_change_seektime_and_cleanup.call(values[:new])
1283 save_undo(_("specify seektime"),
1285 perform_change_seektime_and_cleanup.call(values[:old])
1287 autoscroll_if_needed($autotable_sw, img, textview)
1288 $notebook.set_page(1)
1290 perform_change_seektime_and_cleanup.call(values[:new])
1292 autoscroll_if_needed($autotable_sw, img, textview)
1293 $notebook.set_page(1)
1298 change_seektime_and_cleanup = proc {
1299 rexml_thread_protect {
1300 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1301 change_seektime_and_cleanup_real.call(values)
1306 change_pano_amount_and_cleanup_real = proc { |values|
1307 perform_change_pano_amount_and_cleanup = proc { |val|
1308 cleanup_all_thumbnails.call
1309 rexml_thread_protect {
1310 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1313 perform_change_pano_amount_and_cleanup.call(values[:new])
1315 save_undo(_("change panorama amount"),
1317 perform_change_pano_amount_and_cleanup.call(values[:old])
1319 autoscroll_if_needed($autotable_sw, img, textview)
1320 $notebook.set_page(1)
1322 perform_change_pano_amount_and_cleanup.call(values[:new])
1324 autoscroll_if_needed($autotable_sw, img, textview)
1325 $notebook.set_page(1)
1330 change_pano_amount_and_cleanup = proc {
1331 rexml_thread_protect {
1332 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1333 change_pano_amount_and_cleanup_real.call(values)
1338 whitebalance_and_cleanup_real = proc { |values|
1339 perform_change_whitebalance_and_cleanup = proc { |val|
1340 cleanup_all_thumbnails.call
1341 rexml_thread_protect {
1342 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1343 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1344 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1347 perform_change_whitebalance_and_cleanup.call(values[:new])
1349 save_undo(_("fix white balance"),
1351 perform_change_whitebalance_and_cleanup.call(values[:old])
1353 autoscroll_if_needed($autotable_sw, img, textview)
1354 $notebook.set_page(1)
1356 perform_change_whitebalance_and_cleanup.call(values[:new])
1358 autoscroll_if_needed($autotable_sw, img, textview)
1359 $notebook.set_page(1)
1364 whitebalance_and_cleanup = proc {
1365 rexml_thread_protect {
1366 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1367 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1368 whitebalance_and_cleanup_real.call(values)
1373 gammacorrect_and_cleanup_real = proc { |values|
1374 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1375 cleanup_all_thumbnails.call
1376 rexml_thread_protect {
1377 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1378 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1379 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1382 perform_change_gammacorrect_and_cleanup.call(values[:new])
1384 save_undo(_("gamma correction"),
1386 perform_change_gammacorrect_and_cleanup.call(values[:old])
1388 autoscroll_if_needed($autotable_sw, img, textview)
1389 $notebook.set_page(1)
1391 perform_change_gammacorrect_and_cleanup.call(values[:new])
1393 autoscroll_if_needed($autotable_sw, img, textview)
1394 $notebook.set_page(1)
1399 gammacorrect_and_cleanup = Proc.new {
1400 rexml_thread_protect {
1401 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1402 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1403 gammacorrect_and_cleanup_real.call(values)
1408 enhance_and_cleanup = proc {
1409 perform_enhance_and_cleanup = proc {
1410 cleanup_all_thumbnails.call
1411 rexml_thread_protect {
1412 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1414 my_gen_real_thumbnail.call
1417 cleanup_all_thumbnails.call
1418 perform_enhance_and_cleanup.call
1420 save_undo(_("enhance"),
1422 perform_enhance_and_cleanup.call
1424 autoscroll_if_needed($autotable_sw, img, textview)
1425 $notebook.set_page(1)
1427 perform_enhance_and_cleanup.call
1429 autoscroll_if_needed($autotable_sw, img, textview)
1430 $notebook.set_page(1)
1435 delete = proc { |isacut|
1436 if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed? This operation cannot be undone.")), { :okcancel => true })
1439 perform_delete = proc {
1440 after = autotable.get_next_widget(vbox)
1442 after = autotable.get_previous_widget(vbox)
1444 if $config['deleteondisk'] && !isacut
1445 msg 3, "scheduling for delete: #{fullpath}"
1446 $todelete << fullpath
1448 autotable.remove_widget(vbox)
1450 $vbox2widgets[after][:textview].grab_focus
1451 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1455 previous_pos = autotable.get_current_number(vbox)
1459 delete_current_subalbum
1461 save_undo(_("delete"),
1463 autotable.reinsert(pos, vbox, filename)
1464 $notebook.set_page(1)
1465 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1467 msg 3, "removing deletion schedule of: #{fullpath}"
1468 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1471 $notebook.set_page(1)
1480 $cuts << { :vbox => vbox, :filename => filename }
1481 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1486 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1489 autotable.queue_draws << proc {
1490 $vbox2widgets[last[:vbox]][:textview].grab_focus
1491 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1493 save_undo(_("paste"),
1495 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1496 $notebook.set_page(1)
1499 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1501 $notebook.set_page(1)
1504 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1509 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1510 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1511 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1512 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1514 textview.signal_connect('key-press-event') { |w, event|
1517 x, y = autotable.get_current_pos(vbox)
1518 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1519 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1520 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1521 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1523 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1524 $vbox2widgets[widget_up][:textview].grab_focus
1531 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1533 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1534 $vbox2widgets[widget_down][:textview].grab_focus
1541 if event.keyval == Gdk::Keyval::GDK_Left
1544 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1551 rotate_and_cleanup.call(-90)
1554 if event.keyval == Gdk::Keyval::GDK_Right
1555 next_ = autotable.get_next_widget(vbox)
1556 if next_ && autotable.get_current_pos(next_)[0] > x
1558 $vbox2widgets[next_][:textview].grab_focus
1565 rotate_and_cleanup.call(90)
1568 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1571 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1572 view_element(filename, { :delete => delete })
1575 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1578 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1582 !propagate #- propagate if needed
1585 $ignore_next_release = false
1586 evtbox.signal_connect('button-press-event') { |w, event|
1587 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1588 if event.state & Gdk::Window::BUTTON3_MASK != 0
1589 #- gesture redo: hold right mouse button then click left mouse button
1590 $config['nogestures'] or perform_redo
1591 $ignore_next_release = true
1593 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1595 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1597 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1598 elsif $enhance.active?
1599 enhance_and_cleanup.call
1600 elsif $delete.active?
1604 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1607 $button1_pressed_autotable = true
1608 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1609 if event.state & Gdk::Window::BUTTON1_MASK != 0
1610 #- gesture undo: hold left mouse button then click right mouse button
1611 $config['nogestures'] or perform_undo
1612 $ignore_next_release = true
1614 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1615 view_element(filename, { :delete => delete })
1620 evtbox.signal_connect('button-release-event') { |w, event|
1621 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1622 if !$ignore_next_release
1623 x, y = autotable.get_current_pos(vbox)
1624 next_ = autotable.get_next_widget(vbox)
1625 popup_thumbnail_menu(event, ['delete'], fullpath, type, rexml_thread_protect { $xmldir.elements["*[@filename='#{filename}']"] }, '',
1626 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1627 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1628 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1629 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1630 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1631 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1633 $ignore_next_release = false
1634 $gesture_press = nil
1639 #- handle reordering with drag and drop
1640 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1641 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1642 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1643 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1646 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1648 #- mouse gesture first (dnd disables button-release-event)
1649 if $gesture_press && $gesture_press[:filename] == filename
1650 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1651 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1652 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1653 rotate_and_cleanup.call(angle)
1654 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1656 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1657 msg 3, "gesture delete: click-drag right button to the bottom"
1659 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1664 ctxt.targets.each { |target|
1665 if target.name == 'reorder-elements'
1666 move_dnd = proc { |from,to|
1669 autotable.move(from, to)
1670 save_undo(_("reorder"),
1673 autotable.move(to - 1, from)
1675 autotable.move(to, from + 1)
1677 $notebook.set_page(1)
1679 autotable.move(from, to)
1680 $notebook.set_page(1)
1685 if $multiple_dnd.size == 0
1686 move_dnd.call(selection_data.data.to_i,
1687 autotable.get_current_number(vbox))
1689 UndoHandler.begin_batch
1690 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1692 #- need to update current position between each call
1693 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1694 autotable.get_current_number(vbox))
1696 UndoHandler.end_batch
1707 def create_auto_table
1709 $autotable = Gtk::AutoTable.new(5)
1711 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1712 thumbnails_vb = Gtk::VBox.new(false, 5)
1714 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1715 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1716 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1717 thumbnails_vb.add($autotable)
1719 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1720 $autotable_sw.add_with_viewport(thumbnails_vb)
1722 #- follows stuff for handling multiple elements selection
1723 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1725 update_selected = proc {
1726 $autotable.current_order.each { |path|
1727 w = $name2widgets[path][:evtbox].window
1728 xm = w.position[0] + w.size[0]/2
1729 ym = w.position[1] + w.size[1]/2
1730 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1731 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1732 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1733 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1736 if $selected_elements[path] && ! $selected_elements[path][:keep]
1737 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))
1738 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1739 $selected_elements.delete(path)
1744 $autotable.signal_connect('realize') { |w,e|
1745 gc = Gdk::GC.new($autotable.window)
1746 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1747 gc.function = Gdk::GC::INVERT
1748 #- autoscroll handling for DND and multiple selections
1749 Gtk.timeout_add(100) {
1750 if ! $autotable.window.nil?
1751 w, x, y, mask = $autotable.window.pointer
1752 if mask & Gdk::Window::BUTTON1_MASK != 0
1753 if y < $autotable_sw.vadjustment.value
1755 $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]])
1757 if $button1_pressed_autotable || press_x
1758 scroll_upper($autotable_sw, y)
1761 w, pos_x, pos_y = $autotable.window.pointer
1762 $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]])
1763 update_selected.call
1766 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
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]])
1770 if $button1_pressed_autotable || press_x
1771 scroll_lower($autotable_sw, y)
1774 w, pos_x, pos_y = $autotable.window.pointer
1775 $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]])
1776 update_selected.call
1781 ! $autotable.window.nil?
1785 $autotable.signal_connect('button-press-event') { |w,e|
1787 if !$button1_pressed_autotable
1790 if e.state & Gdk::Window::SHIFT_MASK == 0
1791 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1792 $selected_elements = {}
1793 $statusbar.push(0, utf8(_("Nothing selected.")))
1795 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1797 set_mousecursor(Gdk::Cursor::TCROSS)
1801 $autotable.signal_connect('button-release-event') { |w,e|
1803 if $button1_pressed_autotable
1804 #- unselect all only now
1805 $multiple_dnd = $selected_elements.keys
1806 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1807 $selected_elements = {}
1808 $button1_pressed_autotable = false
1811 $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]])
1812 if $selected_elements.length > 0
1813 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1816 press_x = press_y = pos_x = pos_y = nil
1817 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1821 $autotable.signal_connect('motion-notify-event') { |w,e|
1824 $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]])
1828 $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]])
1829 update_selected.call
1835 def create_subalbums_page
1837 subalbums_hb = Gtk::HBox.new
1838 $subalbums_vb = Gtk::VBox.new(false, 5)
1839 subalbums_hb.pack_start($subalbums_vb, false, false)
1840 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1841 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1842 $subalbums_sw.add_with_viewport(subalbums_hb)
1845 def save_current_file
1851 ios = File.open($filename, "w")
1852 $xmldoc.write(ios, 0)
1854 rescue Iconv::IllegalSequence
1855 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1856 if ! ios.nil? && ! ios.closed?
1859 $xmldoc.xml_decl.encoding = 'UTF-8'
1860 ios = File.open($filename, "w")
1861 $xmldoc.write(ios, 0)
1872 def save_current_file_user
1873 save_tempfilename = $filename
1874 $filename = $orig_filename
1875 if ! save_current_file
1876 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1877 $filename = save_tempfilename
1881 $generated_outofline = false
1882 $filename = save_tempfilename
1884 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1885 $todelete.each { |f|
1890 def mark_document_as_dirty
1891 $xmldoc.elements.each('//dir') { |elem|
1892 elem.delete_attribute('already-generated')
1896 #- ret: true => ok false => cancel
1897 def ask_save_modifications(msg1, msg2, *options)
1899 options = options.size > 0 ? options[0] : {}
1901 if options[:disallow_cancel]
1902 dialog = Gtk::Dialog.new(msg1,
1904 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1905 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1906 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1908 dialog = Gtk::Dialog.new(msg1,
1910 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1911 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1912 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1913 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1915 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1916 dialog.vbox.add(Gtk::Label.new(msg2))
1917 dialog.window_position = Gtk::Window::POS_CENTER
1920 dialog.run { |response|
1922 if response == Gtk::Dialog::RESPONSE_YES
1923 if ! save_current_file_user
1924 return ask_save_modifications(msg1, msg2, options)
1927 #- if we have generated an album but won't save modifications, we must remove
1928 #- already-generated markers in original file
1929 if $generated_outofline
1931 $xmldoc = REXML::Document.new(File.new($orig_filename))
1932 mark_document_as_dirty
1933 ios = File.open($orig_filename, "w")
1934 $xmldoc.write(ios, 0)
1937 puts "exception: #{$!}"
1941 if response == Gtk::Dialog::RESPONSE_CANCEL
1944 $todelete = [] #- unconditionally clear the list of images/videos to delete
1950 def try_quit(*options)
1951 if ask_save_modifications(utf8(_("Save before quitting?")),
1952 utf8(_("Do you want to save your changes before quitting?")),
1958 def show_popup(parent, msg, *options)
1959 dialog = Gtk::Dialog.new
1960 if options[0] && options[0][:title]
1961 dialog.title = options[0][:title]
1963 dialog.title = utf8(_("Booh message"))
1965 lbl = Gtk::Label.new
1966 if options[0] && options[0][:nomarkup]
1971 if options[0] && options[0][:centered]
1972 lbl.set_justify(Gtk::Justification::CENTER)
1974 if options[0] && options[0][:selectable]
1975 lbl.selectable = true
1977 if options[0] && options[0][:topwidget]
1978 dialog.vbox.add(options[0][:topwidget])
1980 if options[0] && options[0][:scrolled]
1981 sw = Gtk::ScrolledWindow.new(nil, nil)
1982 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1983 sw.add_with_viewport(lbl)
1985 dialog.set_default_size(500, 600)
1987 dialog.vbox.add(lbl)
1988 dialog.set_default_size(200, 120)
1990 if options[0] && options[0][:okcancel]
1991 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1993 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1995 if options[0] && options[0][:pos_centered]
1996 dialog.window_position = Gtk::Window::POS_CENTER
1998 dialog.window_position = Gtk::Window::POS_MOUSE
2001 if options[0] && options[0][:linkurl]
2002 linkbut = Gtk::Button.new('')
2003 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
2004 linkbut.signal_connect('clicked') {
2005 open_url(options[0][:linkurl])
2006 dialog.response(Gtk::Dialog::RESPONSE_OK)
2007 set_mousecursor_normal
2009 linkbut.relief = Gtk::RELIEF_NONE
2010 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
2011 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
2012 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
2017 if !options[0] || !options[0][:not_transient]
2018 dialog.transient_for = parent
2019 dialog.run { |response|
2021 if options[0] && options[0][:okcancel]
2022 return response == Gtk::Dialog::RESPONSE_OK
2026 dialog.signal_connect('response') { dialog.destroy }
2030 def set_mainwindow_title(progress)
2031 filename = $orig_filename || $filename
2034 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
2036 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
2040 $main_window.title = 'booh - ' + File.basename(filename)
2042 $main_window.title = 'booh'
2047 def backend_wait_message(parent, msg, infopipe_path, mode)
2049 w.set_transient_for(parent)
2052 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2053 vb.pack_start(Gtk::Label.new(msg), false, false)
2055 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
2056 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
2057 if mode != 'one dir scan'
2058 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2060 if mode == 'web-album'
2061 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2062 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2064 vb.pack_start(Gtk::HSeparator.new, false, false)
2066 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2067 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2068 vb.pack_end(bottom, false, false)
2071 update_progression_title_pb1 = proc {
2072 if mode == 'web-album'
2073 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2074 elsif mode != 'one dir scan'
2075 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2077 set_mainwindow_title(pb1_1.fraction)
2081 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2082 refresh_thread = Thread.new {
2083 directories_counter = 0
2084 while line = infopipe.gets
2085 msg 3, "infopipe got data: #{line}"
2086 if line =~ /^directories: (\d+), sizes: (\d+)/
2087 directories = $1.to_f + 1
2089 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2090 elements = $3.to_f + 1
2091 if mode == 'web-album'
2095 gtk_thread_protect { pb1_1.fraction = 0 }
2096 if mode != 'one dir scan'
2097 newtext = utf8(full_src_dir_to_rel($1, $2))
2098 newtext = '/' if newtext == ''
2099 gtk_thread_protect { pb1_2.text = newtext }
2100 directories_counter += 1
2101 gtk_thread_protect {
2102 pb1_2.fraction = directories_counter / directories
2103 update_progression_title_pb1.call
2106 elsif line =~ /^processing element$/
2107 element_counter += 1
2108 gtk_thread_protect {
2109 pb1_1.fraction = element_counter / elements
2110 update_progression_title_pb1.call
2112 elsif line =~ /^processing size$/
2113 element_counter += 1
2114 gtk_thread_protect {
2115 pb1_1.fraction = element_counter / elements
2116 update_progression_title_pb1.call
2118 elsif line =~ /^finished processing sizes$/
2119 gtk_thread_protect { pb1_1.fraction = 1 }
2120 elsif line =~ /^creating index.html$/
2121 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2122 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2123 directories_counter = 0
2124 elsif line =~ /^index.html: (.+)\|(.+)/
2125 newtext = utf8(full_src_dir_to_rel($1, $2))
2126 newtext = '/' if newtext == ''
2127 gtk_thread_protect { pb2.text = newtext }
2128 directories_counter += 1
2129 gtk_thread_protect {
2130 pb2.fraction = directories_counter / directories
2131 set_mainwindow_title(0.9 + pb2.fraction / 10)
2133 elsif line =~ /^die: (.*)$/
2140 w.signal_connect('delete-event') { w.destroy }
2141 w.signal_connect('destroy') {
2142 Thread.kill(refresh_thread)
2143 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2146 File.delete(infopipe_path)
2148 set_mainwindow_title(nil)
2150 w.window_position = Gtk::Window::POS_CENTER
2156 def call_backend(cmd, waitmsg, mode, params)
2157 pipe = Tempfile.new("boohpipe")
2158 Thread.critical = true
2161 system("mkfifo #{path}")
2162 Thread.critical = false
2163 cmd += " --info-pipe #{path}"
2164 button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2169 id, exitstatus = Process.waitpid2(pid)
2170 gtk_thread_protect { w8.destroy }
2172 if params[:successmsg]
2173 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2175 if params[:closure_after]
2176 gtk_thread_protect(¶ms[:closure_after])
2178 elsif exitstatus == 15
2179 #- say nothing, user aborted
2181 gtk_thread_protect { show_popup($main_window,
2182 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2188 button.signal_connect('clicked') {
2189 Process.kill('SIGTERM', pid)
2193 def save_changes(*forced)
2194 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2198 $xmldir.delete_attribute('already-generated')
2200 propagate_children = proc { |xmldir|
2201 if xmldir.attributes['subdirs-caption']
2202 xmldir.delete_attribute('already-generated')
2204 xmldir.elements.each('dir') { |element|
2205 propagate_children.call(element)
2209 if $xmldir.child_byname_notattr('dir', 'deleted')
2210 new_title = $subalbums_title.buffer.text
2211 if new_title != $xmldir.attributes['subdirs-caption']
2212 parent = $xmldir.parent
2213 if parent.name == 'dir'
2214 parent.delete_attribute('already-generated')
2216 propagate_children.call($xmldir)
2218 $xmldir.add_attribute('subdirs-caption', new_title)
2219 $xmldir.elements.each('dir') { |element|
2220 if !element.attributes['deleted']
2221 path = element.attributes['path']
2222 newtext = $subalbums_edits[path][:editzone].buffer.text
2223 if element.attributes['subdirs-caption']
2224 if element.attributes['subdirs-caption'] != newtext
2225 propagate_children.call(element)
2227 element.add_attribute('subdirs-caption', newtext)
2228 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2230 if element.attributes['thumbnails-caption'] != newtext
2231 element.delete_attribute('already-generated')
2233 element.add_attribute('thumbnails-caption', newtext)
2234 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2240 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2241 if $xmldir.attributes['thumbnails-caption']
2242 path = $xmldir.attributes['path']
2243 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2245 elsif $xmldir.attributes['thumbnails-caption']
2246 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2249 if $xmldir.attributes['thumbnails-caption']
2250 if edit = $subalbums_edits[$xmldir.attributes['path']]
2251 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2255 #- remove and reinsert elements to reflect new ordering
2258 $xmldir.elements.each { |element|
2259 if element.name == 'image' || element.name == 'video'
2260 saves[element.attributes['filename']] = element.remove
2264 $autotable.current_order.each { |path|
2265 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2266 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2269 saves.each_key { |path|
2270 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2271 chld.add_attribute('deleted', 'true')
2275 def sort_by_exif_date
2279 rexml_thread_protect {
2280 $xmldir.elements.each { |element|
2281 if element.name == 'image' || element.name == 'video'
2282 current_order << element.attributes['filename']
2287 #- look for EXIF dates
2290 if current_order.size > 20
2292 w.set_transient_for($main_window)
2294 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2295 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2296 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2297 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2298 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2299 vb.pack_end(bottom, false, false)
2301 w.signal_connect('delete-event') { w.destroy }
2302 w.window_position = Gtk::Window::POS_CENTER
2306 b.signal_connect('clicked') { aborted = true }
2308 current_order.each { |f|
2310 if entry2type(f) == 'image'
2312 pb.fraction = i.to_f / current_order.size
2313 Gtk.main_iteration while Gtk.events_pending?
2314 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2316 dates[f] = date_time
2329 current_order.each { |f|
2330 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2332 dates[f] = date_time
2338 rexml_thread_protect {
2339 $xmldir.elements.each { |element|
2340 if element.name == 'image' || element.name == 'video'
2341 saves[element.attributes['filename']] = element.remove
2346 neworder = smartsort(current_order, dates)
2348 rexml_thread_protect {
2350 $xmldir.add_element(saves[f].name, saves[f].attributes)
2354 #- let the auto-table reflect new ordering
2358 def remove_all_captions
2361 $autotable.current_order.each { |path|
2362 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2363 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2365 save_undo(_("remove all captions"),
2367 texts.each_key { |key|
2368 $name2widgets[key][:textview].buffer.text = texts[key]
2370 $notebook.set_page(1)
2372 texts.each_key { |key|
2373 $name2widgets[key][:textview].buffer.text = ''
2375 $notebook.set_page(1)
2381 $selected_elements.each_key { |path|
2382 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2388 $selected_elements = {}
2392 $undo_tb.sensitive = $undo_mb.sensitive = false
2393 $redo_tb.sensitive = $redo_mb.sensitive = false
2399 $subalbums_vb.children.each { |chld|
2400 $subalbums_vb.remove(chld)
2402 $subalbums = Gtk::Table.new(0, 0, true)
2403 current_y_sub_albums = 0
2405 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2406 $subalbums_edits = {}
2407 subalbums_counter = 0
2408 subalbums_edits_bypos = {}
2410 add_subalbum = proc { |xmldir, counter|
2411 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2412 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2413 if xmldir == $xmldir
2414 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2415 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2416 caption = xmldir.attributes['thumbnails-caption']
2417 infotype = 'thumbnails'
2419 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2420 captionfile, caption = find_subalbum_caption_info(xmldir)
2421 infotype = find_subalbum_info_type(xmldir)
2423 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2424 hbox = Gtk::HBox.new
2425 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2427 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2430 my_gen_real_thumbnail = proc {
2431 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2434 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2435 f.add(img = Gtk::Image.new)
2436 my_gen_real_thumbnail.call
2438 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2440 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2441 $subalbums.attach(hbox,
2442 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2444 frame, textview = create_editzone($subalbums_sw, 0, img)
2445 textview.buffer.text = caption
2446 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2447 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2449 change_image = proc {
2450 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2452 Gtk::FileChooser::ACTION_OPEN,
2454 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2455 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2456 fc.transient_for = $main_window
2457 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))
2458 f.add(preview_img = Gtk::Image.new)
2460 fc.signal_connect('update-preview') { |w|
2462 if fc.preview_filename
2463 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2464 fc.preview_widget_active = true
2466 rescue Gdk::PixbufError
2467 fc.preview_widget_active = false
2470 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2472 old_file = captionfile
2473 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2474 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2475 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2476 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2478 new_file = fc.filename
2479 msg 3, "new captionfile is: #{fc.filename}"
2480 perform_changefile = proc {
2481 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2482 $modified_pixbufs.delete(thumbnail_file)
2483 xmldir.delete_attribute("#{infotype}-rotate")
2484 xmldir.delete_attribute("#{infotype}-color-swap")
2485 xmldir.delete_attribute("#{infotype}-enhance")
2486 xmldir.delete_attribute("#{infotype}-seektime")
2487 my_gen_real_thumbnail.call
2489 perform_changefile.call
2491 save_undo(_("change caption file for sub-album"),
2493 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2494 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2495 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2496 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2497 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2498 my_gen_real_thumbnail.call
2499 $notebook.set_page(0)
2501 perform_changefile.call
2502 $notebook.set_page(0)
2510 if File.exists?(thumbnail_file)
2511 File.delete(thumbnail_file)
2513 my_gen_real_thumbnail.call
2516 rotate_and_cleanup = proc { |angle|
2517 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2518 if File.exists?(thumbnail_file)
2519 File.delete(thumbnail_file)
2523 move = proc { |direction|
2526 save_changes('forced')
2527 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2528 if direction == 'up'
2529 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2530 subalbums_edits_bypos[oldpos - 1][:position] += 1
2532 if direction == 'down'
2533 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2534 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2536 if direction == 'top'
2537 for i in 1 .. oldpos - 1
2538 subalbums_edits_bypos[i][:position] += 1
2540 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2542 if direction == 'bottom'
2543 for i in oldpos + 1 .. subalbums_counter
2544 subalbums_edits_bypos[i][:position] -= 1
2546 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2550 $xmldir.elements.each('dir') { |element|
2551 if (!element.attributes['deleted'])
2552 elems << [ element.attributes['path'], element.remove ]
2555 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2556 each { |e| $xmldir.add_element(e[1]) }
2557 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2558 $xmldir.elements.each('descendant::dir') { |elem|
2559 elem.delete_attribute('already-generated')
2562 sel = $albums_tv.selection.selected_rows
2564 populate_subalbums_treeview(false)
2565 $albums_tv.selection.select_path(sel[0])
2568 color_swap_and_cleanup = proc {
2569 perform_color_swap_and_cleanup = proc {
2570 color_swap(xmldir, "#{infotype}-")
2571 my_gen_real_thumbnail.call
2573 perform_color_swap_and_cleanup.call
2575 save_undo(_("color swap"),
2577 perform_color_swap_and_cleanup.call
2578 $notebook.set_page(0)
2580 perform_color_swap_and_cleanup.call
2581 $notebook.set_page(0)
2586 change_seektime_and_cleanup = proc {
2587 if values = ask_new_seektime(xmldir, "#{infotype}-")
2588 perform_change_seektime_and_cleanup = proc { |val|
2589 change_seektime(xmldir, "#{infotype}-", val)
2590 my_gen_real_thumbnail.call
2592 perform_change_seektime_and_cleanup.call(values[:new])
2594 save_undo(_("specify seektime"),
2596 perform_change_seektime_and_cleanup.call(values[:old])
2597 $notebook.set_page(0)
2599 perform_change_seektime_and_cleanup.call(values[:new])
2600 $notebook.set_page(0)
2606 whitebalance_and_cleanup = proc {
2607 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2608 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2609 perform_change_whitebalance_and_cleanup = proc { |val|
2610 change_whitebalance(xmldir, "#{infotype}-", val)
2611 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2612 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2613 if File.exists?(thumbnail_file)
2614 File.delete(thumbnail_file)
2617 perform_change_whitebalance_and_cleanup.call(values[:new])
2619 save_undo(_("fix white balance"),
2621 perform_change_whitebalance_and_cleanup.call(values[:old])
2622 $notebook.set_page(0)
2624 perform_change_whitebalance_and_cleanup.call(values[:new])
2625 $notebook.set_page(0)
2631 gammacorrect_and_cleanup = proc {
2632 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2633 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2634 perform_change_gammacorrect_and_cleanup = proc { |val|
2635 change_gammacorrect(xmldir, "#{infotype}-", val)
2636 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2637 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2638 if File.exists?(thumbnail_file)
2639 File.delete(thumbnail_file)
2642 perform_change_gammacorrect_and_cleanup.call(values[:new])
2644 save_undo(_("gamma correction"),
2646 perform_change_gammacorrect_and_cleanup.call(values[:old])
2647 $notebook.set_page(0)
2649 perform_change_gammacorrect_and_cleanup.call(values[:new])
2650 $notebook.set_page(0)
2656 enhance_and_cleanup = proc {
2657 perform_enhance_and_cleanup = proc {
2658 enhance(xmldir, "#{infotype}-")
2659 my_gen_real_thumbnail.call
2662 perform_enhance_and_cleanup.call
2664 save_undo(_("enhance"),
2666 perform_enhance_and_cleanup.call
2667 $notebook.set_page(0)
2669 perform_enhance_and_cleanup.call
2670 $notebook.set_page(0)
2675 evtbox.signal_connect('button-press-event') { |w, event|
2676 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2678 rotate_and_cleanup.call(90)
2680 rotate_and_cleanup.call(-90)
2681 elsif $enhance.active?
2682 enhance_and_cleanup.call
2685 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2686 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2687 { :forbid_left => true, :forbid_right => true,
2688 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2689 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2690 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2691 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2692 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2694 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2699 evtbox.signal_connect('button-press-event') { |w, event|
2700 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2704 evtbox.signal_connect('button-release-event') { |w, event|
2705 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2706 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2707 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2708 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2709 msg 3, "gesture rotate: #{angle}"
2710 rotate_and_cleanup.call(angle)
2713 $gesture_press = nil
2716 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2717 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2718 current_y_sub_albums += 1
2721 if $xmldir.child_byname_notattr('dir', 'deleted')
2723 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2724 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] || ''
2725 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2726 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2727 #- this album image/caption
2728 if $xmldir.attributes['thumbnails-caption']
2729 add_subalbum.call($xmldir, 0)
2732 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2733 $xmldir.elements.each { |element|
2734 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2735 #- element (image or video) of this album
2736 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2737 msg 3, "dest_img: #{dest_img}"
2738 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2739 total[element.name] += 1
2741 if element.name == 'dir' && !element.attributes['deleted']
2742 #- sub-album image/caption
2743 add_subalbum.call(element, subalbums_counter += 1)
2744 total[element.name] += 1
2747 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2748 total['image'], total['video'], total['dir'] ]))
2749 $subalbums_vb.add($subalbums)
2750 $subalbums_vb.show_all
2752 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2753 $notebook.get_tab_label($autotable_sw).sensitive = false
2754 $notebook.set_page(0)
2755 $thumbnails_title.buffer.text = ''
2757 $notebook.get_tab_label($autotable_sw).sensitive = true
2758 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2761 if !$xmldir.child_byname_notattr('dir', 'deleted')
2762 $notebook.get_tab_label($subalbums_sw).sensitive = false
2763 $notebook.set_page(1)
2765 $notebook.get_tab_label($subalbums_sw).sensitive = true
2769 def pixbuf_or_nil(filename)
2771 return Gdk::Pixbuf.new(filename)
2777 def theme_choose(current)
2778 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2780 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2781 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2782 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2784 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2785 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2786 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2787 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2788 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2789 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2790 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2791 treeview.signal_connect('button-press-event') { |w, event|
2792 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2793 dialog.response(Gtk::Dialog::RESPONSE_OK)
2797 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2799 ([ $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|
2802 iter[0] = File.basename(dir)
2803 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2804 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2805 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2806 if File.basename(dir) == current
2807 treeview.selection.select_iter(iter)
2810 dialog.set_default_size(-1, 500)
2811 dialog.vbox.show_all
2813 dialog.run { |response|
2814 iter = treeview.selection.selected
2816 if response == Gtk::Dialog::RESPONSE_OK && iter
2817 return model.get_value(iter, 0)
2823 def show_password_protections
2824 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2825 child_iter = $albums_iters[xmldir.attributes['path']]
2826 if xmldir.attributes['password-protect']
2827 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2828 already_protected = true
2829 elsif already_protected
2830 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2832 pix = pix.saturate_and_pixelate(1, true)
2838 xmldir.elements.each('dir') { |elem|
2839 if !elem.attributes['deleted']
2840 examine_dir_elem.call(child_iter, elem, already_protected)
2844 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2847 def populate_subalbums_treeview(select_first)
2851 $subalbums_vb.children.each { |chld|
2852 $subalbums_vb.remove(chld)
2855 source = $xmldoc.root.attributes['source']
2856 msg 3, "source: #{source}"
2858 xmldir = $xmldoc.elements['//dir']
2859 if !xmldir || xmldir.attributes['path'] != source
2860 msg 1, _("Corrupted booh file...")
2864 append_dir_elem = proc { |parent_iter, xmldir|
2865 child_iter = $albums_ts.append(parent_iter)
2866 child_iter[0] = File.basename(xmldir.attributes['path'])
2867 child_iter[1] = xmldir.attributes['path']
2868 $albums_iters[xmldir.attributes['path']] = child_iter
2869 msg 3, "puttin location: #{xmldir.attributes['path']}"
2870 xmldir.elements.each('dir') { |elem|
2871 if !elem.attributes['deleted']
2872 append_dir_elem.call(child_iter, elem)
2876 append_dir_elem.call(nil, xmldir)
2877 show_password_protections
2879 $albums_tv.expand_all
2881 $albums_tv.selection.select_iter($albums_ts.iter_first)
2885 def select_current_theme
2886 select_theme($xmldoc.root.attributes['theme'],
2887 $xmldoc.root.attributes['limit-sizes'],
2888 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2889 $xmldoc.root.attributes['thumbnails-per-row'])
2892 def open_file(filename)
2896 $current_path = nil #- invalidate
2897 $modified_pixbufs = {}
2900 $subalbums_vb.children.each { |chld|
2901 $subalbums_vb.remove(chld)
2904 if !File.exists?(filename)
2905 return utf8(_("File not found."))
2909 $xmldoc = REXML::Document.new(File.new(filename))
2914 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2915 if entry2type(filename).nil?
2916 return utf8(_("Not a booh file!"))
2918 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."))
2922 if !source = $xmldoc.root.attributes['source']
2923 return utf8(_("Corrupted booh file..."))
2926 if !dest = $xmldoc.root.attributes['destination']
2927 return utf8(_("Corrupted booh file..."))
2930 if !theme = $xmldoc.root.attributes['theme']
2931 return utf8(_("Corrupted booh file..."))
2934 if $xmldoc.root.attributes['version'] < '0.9.0'
2935 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2936 mark_document_as_dirty
2937 if $xmldoc.root.attributes['version'] < '0.8.4'
2938 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2939 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2940 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2941 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2942 if old_dest_dir != new_dest_dir
2943 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2945 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2946 xmldir.elements.each { |element|
2947 if %w(image video).include?(element.name) && !element.attributes['deleted']
2948 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2949 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2950 Dir[old_name + '*'].each { |file|
2951 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2952 file != new_file and sys("mv '#{file}' '#{new_file}'")
2955 if element.name == 'dir' && !element.attributes['deleted']
2956 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2957 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2958 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2962 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2966 $xmldoc.root.add_attribute('version', $VERSION)
2969 select_current_theme
2971 $filename = filename
2972 set_mainwindow_title(nil)
2973 $default_size['thumbnails'] =~ /(.*)x(.*)/
2974 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2975 $albums_thumbnail_size =~ /(.*)x(.*)/
2976 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2978 populate_subalbums_treeview(true)
2980 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $extend.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
2984 def open_file_user(filename)
2985 result = open_file(filename)
2987 $config['last-opens'] ||= []
2988 if $config['last-opens'][-1] != utf8(filename)
2989 $config['last-opens'] << utf8(filename)
2991 $orig_filename = $filename
2992 $main_window.title = 'booh - ' + File.basename($orig_filename)
2993 tmp = Tempfile.new("boohtemp")
2994 Thread.critical = true
2995 $filename = tmp.path
2998 ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
2999 Thread.critical = false
3001 $tempfiles << $filename << "#{$filename}.backup"
3003 $orig_filename = nil
3009 if !ask_save_modifications(utf8(_("Save this album?")),
3010 utf8(_("Do you want to save the changes to this album?")),
3011 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3014 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
3016 Gtk::FileChooser::ACTION_OPEN,
3018 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3019 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3020 fc.set_current_folder(File.expand_path("~/.booh"))
3021 fc.transient_for = $main_window
3024 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3025 push_mousecursor_wait(fc)
3026 msg = open_file_user(fc.filename)
3041 def additional_booh_options
3044 options += "--mproc #{$config['mproc'].to_i} "
3046 options += "--comments-format '#{$config['comments-format']}' "
3047 if $config['transcode-videos']
3048 options += "--transcode-videos '#{$config['transcode-videos']}' "
3053 def ask_multi_languages(value)
3055 spl = value.split(',')
3056 value = [ spl[0..-2], spl[-1] ]
3059 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
3062 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3063 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3065 lbl = Gtk::Label.new
3067 _("You can choose to activate <b>multi-languages</b> support for this web-album
3068 (it will work only if you publish your web-album on an Apache web-server). This will
3069 use the MultiViews feature of Apache; the pages will be served according to the
3070 value of the Accept-Language HTTP header sent by the web browsers, so that people
3071 with different languages preferences will be able to browse your web-album with
3072 navigation in their language (if language is available).
3075 dialog.vbox.add(lbl)
3076 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3077 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3078 add(languages = Gtk::Button.new))))
3080 pick_languages = proc {
3081 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3084 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3085 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3087 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3088 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3090 SUPPORTED_LANGUAGES.each { |lang|
3091 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3092 if ! value.nil? && value[0].include?(lang)
3098 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3099 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3100 fallback_language = nil
3101 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3102 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3103 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3104 fbl_rb.active = true
3105 fallback_language = SUPPORTED_LANGUAGES[0]
3107 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3108 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3109 rb.signal_connect('clicked') { fallback_language = lang }
3110 if ! value.nil? && value[1] == lang
3115 dialog2.window_position = Gtk::Window::POS_MOUSE
3119 dialog2.run { |response|
3121 if resp == Gtk::Dialog::RESPONSE_OK
3123 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3124 value[1] = fallback_language
3125 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3132 languages.signal_connect('clicked') {
3135 dialog.window_position = Gtk::Window::POS_MOUSE
3139 rb_yes.active = true
3140 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3142 rb_no.signal_connect('clicked') {
3146 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3159 dialog.run { |response|
3164 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3166 return [ true, nil ]
3168 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3177 if !ask_save_modifications(utf8(_("Save this album?")),
3178 utf8(_("Do you want to save the changes to this album?")),
3179 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3182 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3184 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3185 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3186 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3188 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3189 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3190 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3191 tbl.attach(src = Gtk::Entry.new,
3192 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3193 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3194 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3195 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3196 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3197 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3198 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3199 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3200 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3201 tbl.attach(dest = Gtk::Entry.new,
3202 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3203 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3204 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3205 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3206 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3207 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3208 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3209 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3210 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3212 tooltips = Gtk::Tooltips.new
3213 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3214 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3215 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3216 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3217 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3218 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3219 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)
3220 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3221 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3222 nperpage_model = Gtk::ListStore.new(String, String)
3223 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3224 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3225 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3226 nperpagecombo.set_attributes(crt, { :markup => 0 })
3227 iter = nperpage_model.append
3228 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3230 [ 12, 20, 30, 40, 50 ].each { |v|
3231 iter = nperpage_model.append
3232 iter[0] = iter[1] = v.to_s
3234 nperpagecombo.active = 0
3236 multilanguages_value = nil
3237 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3238 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3239 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)
3240 multilanguages.signal_connect('clicked') {
3241 retval = ask_multi_languages(multilanguages_value)
3243 multilanguages_value = retval[1]
3245 if multilanguages_value
3246 ml_label.text = utf8(_("Multi-languages: enabled."))
3248 ml_label.text = utf8(_("Multi-languages: disabled."))
3251 if $config['default-multi-languages']
3252 multilanguages_value = $config['default-multi-languages']
3253 ml_label.text = utf8(_("Multi-languages: enabled."))
3255 ml_label.text = utf8(_("Multi-languages: disabled."))
3258 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3259 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3260 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)
3261 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3262 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3263 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)
3265 src_nb_calculated_for = ''
3267 process_src_nb = proc {
3268 if src.text != src_nb_calculated_for
3269 src_nb_calculated_for = src.text
3271 Thread.kill(src_nb_thread)
3274 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3275 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3277 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3278 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3279 src_nb_thread = Thread.new {
3280 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3281 total = { 'image' => 0, 'video' => 0, nil => 0 }
3282 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3283 if File.basename(dir) =~ /^\./
3287 Dir.entries(dir.chomp).each { |file|
3288 total[entry2type(file)] += 1
3290 rescue Errno::EACCES, Errno::ENOENT
3294 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3298 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3301 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3307 timeout_src_nb = Gtk.timeout_add(100) {
3311 src_browse.signal_connect('clicked') {
3312 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3314 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3316 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3317 fc.transient_for = $main_window
3318 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3319 src.text = utf8(fc.filename)
3321 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3326 dest_browse.signal_connect('clicked') {
3327 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3329 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3331 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3332 fc.transient_for = $main_window
3333 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3334 dest.text = utf8(fc.filename)
3339 conf_browse.signal_connect('clicked') {
3340 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3342 Gtk::FileChooser::ACTION_SAVE,
3344 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3345 fc.transient_for = $main_window
3346 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3347 fc.set_current_folder(File.expand_path("~/.booh"))
3348 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3349 conf.text = utf8(fc.filename)
3356 recreate_theme_config = proc {
3357 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3359 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3360 $images_size.each { |s|
3361 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3365 tooltips.set_tip(cb, utf8(s['description']), nil)
3366 theme_sizes << { :widget => cb, :value => s['name'] }
3368 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3369 tooltips = Gtk::Tooltips.new
3370 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3371 theme_sizes << { :widget => cb, :value => 'original' }
3374 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3377 $allowed_N_values.each { |n|
3379 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3381 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3383 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3387 nperrows << { :widget => rb, :value => n }
3389 nperrowradios.show_all
3391 recreate_theme_config.call
3393 theme_button.signal_connect('clicked') {
3394 if newtheme = theme_choose(theme_button.label)
3395 theme_button.label = newtheme
3396 recreate_theme_config.call
3400 dialog.vbox.add(frame1)
3401 dialog.vbox.add(frame2)
3407 dialog.run { |response|
3408 if response == Gtk::Dialog::RESPONSE_OK
3409 srcdir = from_utf8_safe(src.text)
3410 destdir = from_utf8_safe(dest.text)
3411 confpath = from_utf8_safe(conf.text)
3412 if src.text != '' && srcdir == ''
3413 show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3415 elsif !File.directory?(srcdir)
3416 show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3418 elsif dest.text != '' && destdir == ''
3419 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3421 elsif destdir != make_dest_filename(destdir)
3422 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3424 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3425 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3426 inside it will be permanently removed before creating the web-album!
3427 Are you sure you want to continue?")), { :okcancel => true })
3429 elsif File.exists?(destdir) && !File.directory?(destdir)
3430 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3432 elsif conf.text == ''
3433 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3435 elsif conf.text != '' && confpath == ''
3436 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3438 elsif File.directory?(confpath)
3439 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3441 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3442 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3444 system("mkdir '#{destdir}'")
3445 if !File.directory?(destdir)
3446 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3458 srcdir = from_utf8(src.text)
3459 destdir = from_utf8(dest.text)
3460 configskel = File.expand_path(from_utf8(conf.text))
3461 theme = theme_button.label
3462 #- some sort of automatic theme preference
3463 $config['default-theme'] = theme
3464 $config['default-multi-languages'] = multilanguages_value
3465 $config['default-optimize32'] = optimize432.active?.to_s
3466 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3467 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3468 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3469 opt432 = optimize432.active?
3470 madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3471 indexlink = indexlinkentry.text.gsub('\'', ''')
3474 Thread.kill(src_nb_thread)
3475 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3478 Gtk.timeout_remove(timeout_src_nb)
3481 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3482 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3483 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3484 (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
3485 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3486 utf8(_("Please wait while scanning source directory...")),
3488 { :closure_after => proc {
3489 open_file_user(configskel)
3490 $main_window.urgency_hint = true
3496 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3498 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3499 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3500 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3502 source = $xmldoc.root.attributes['source']
3503 dest = $xmldoc.root.attributes['destination']
3504 theme = $xmldoc.root.attributes['theme']
3505 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3506 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3507 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3508 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3510 limit_sizes = limit_sizes.split(/,/)
3512 madewith = ($xmldoc.root.attributes['made-with'] || '').gsub(''', '\'')
3513 indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub(''', '\'')
3514 save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3516 tooltips = Gtk::Tooltips.new
3517 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3518 tbl.attach(Gtk::Label.new(utf8(_("Directory of source photos/videos: "))),
3519 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3520 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),