5 # A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
13 # Copyright (c) 2004-2008 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
27 require 'booh/libadds'
28 require 'booh/GtkAutoTable'
32 bindtextdomain("booh")
34 require 'booh/rexml/document'
37 require 'booh/booh-lib'
39 require 'booh/UndoHandler'
40 require 'booh/Synchronizator'
45 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
47 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
50 #- default values for some globals
54 $ignore_videos = false
55 $button1_pressed_autotable = false
56 $generated_outofline = false
59 puts _("Usage: %s [OPTION]...") % File.basename($0)
61 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
66 parser = GetoptLong.new
67 parser.set_options(*$options.collect { |ary| ary[0..2] })
69 parser.each_option do |name, arg|
75 when '--verbose-level'
76 $verbose_level = arg.to_i
89 $config_file = File.expand_path('~/.booh-gui-rc')
90 if File.readable?($config_file)
91 $xmldoc = REXML::Document.new(File.new($config_file))
92 $xmldoc.root.elements.each { |element|
93 txt = element.get_text
95 if txt.value =~ /~~~/ || element.name == 'last-opens'
96 $config[element.name] = txt.value.split(/~~~/)
98 $config[element.name] = txt.value
100 elsif element.elements.size == 0
101 $config[element.name] = ''
103 $config[element.name] = {}
104 element.each { |chld|
106 $config[element.name][chld.name] = txt ? txt.value : nil
111 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
112 $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
113 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
114 $config['comments-format'] ||= '%t'
115 if !FileTest.directory?(File.expand_path('~/.booh'))
116 system("mkdir ~/.booh")
118 if $config['mproc'].nil?
120 for line in IO.readlines('/proc/cpuinfo') do
121 line =~ /^processor/ and cpus += 1
124 $config['mproc'] = cpus
127 $config['rotate-set-exif'] ||= 'true'
133 if !system("which convert >/dev/null 2>/dev/null")
134 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
135 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
138 if !system("which identify >/dev/null 2>/dev/null")
139 show_popup($main_window, utf8(_("The program 'identify' is needed to get photos sizes and EXIF data. Please install it.
140 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
142 if !system("which exif >/dev/null 2>/dev/null")
143 show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
145 missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
147 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
150 viewer_binary = $config['video-viewer'].split.first
151 if viewer_binary && !File.executable?(viewer_binary)
152 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
153 You should fix this in Edit/Preferences so that you can view videos.
155 Problem was: '%s' is not an executable file.
156 Hint: don't forget to specify the full path to the executable,
157 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
159 image_editor_binary = $config['image-editor'].split.first
160 if image_editor_binary && !File.executable?(image_editor_binary)
161 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
162 You should fix this in Edit/Preferences so that you can edit photos externally.
164 Problem was: '%s' is not an executable file.
165 Hint: don't forget to specify the full path to the executable,
166 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
168 browser_binary = $config['browser'].split.first
169 if browser_binary && !File.executable?(browser_binary)
170 show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
171 You should fix this in Edit/Preferences so that you can open URLs.
173 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
178 if $config['last-opens'] && $config['last-opens'].size > 10
179 $config['last-opens'] = $config['last-opens'][-10, 10]
182 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
183 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
184 $config.each_pair { |key, value|
185 elem = $xmldoc.root.add_element key
187 $config[key].each_pair { |subkey, subvalue|
188 subelem = elem.add_element subkey
189 subelem.add_text subvalue.to_s
191 elsif value.is_a? Array
192 elem.add_text value.join('~~~')
197 elem.add_text value.to_s
201 ios = File.open($config_file, "w")
202 $xmldoc.write(ios, 0)
205 $tempfiles.each { |f|
212 def set_mousecursor(what, *widget)
213 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
214 if widget[0] && widget[0].window
215 widget[0].window.cursor = cursor
217 if $main_window && $main_window.window
218 $main_window.window.cursor = cursor
220 $current_cursor = what
222 def set_mousecursor_wait(*widget)
223 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
224 if Thread.current == Thread.main
225 Gtk.main_iteration while Gtk.events_pending?
228 def set_mousecursor_normal(*widget)
229 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
231 def push_mousecursor_wait(*widget)
232 if $current_cursor != Gdk::Cursor::WATCH
233 $save_cursor = $current_cursor
234 gtk_thread_protect { set_mousecursor_wait(*widget) }
237 def pop_mousecursor(*widget)
238 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
242 source = $xmldoc.root.attributes['source']
243 dest = $xmldoc.root.attributes['destination']
244 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
247 def full_src_dir_to_rel(path, source)
248 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
251 def build_full_dest_filename(filename)
252 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
255 def save_undo(name, closure, *params)
256 UndoHandler.save_undo(name, closure, [ *params ])
257 $undo_tb.sensitive = $undo_mb.sensitive = true
258 $redo_tb.sensitive = $redo_mb.sensitive = false
261 def view_element(filename, closures)
262 if entry2type(filename) == 'video'
263 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
269 w = create_window.set_title(filename)
271 msg 3, "filename: #{filename}"
272 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
273 #- typically this file won't exist in case of videos; try with the largest thumbnail around
274 if !File.exists?(dest_img)
275 if entry2type(filename) == 'video'
276 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
277 if not alternatives.empty?
278 dest_img = alternatives[-1]
281 push_mousecursor_wait
282 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
284 if !File.exists?(dest_img)
285 msg 2, _("Could not generate fullscreen thumbnail!")
290 evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(Gtk::Image.new(dest_img)).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
291 evt.signal_connect('button-press-event') { |this, event|
292 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
293 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
295 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
297 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
298 delete_item.signal_connect('activate') {
300 closures[:delete].call(false)
303 menu.popup(nil, nil, event.button, event.time)
306 evt.signal_connect('button-release-event') { |this, event|
308 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
309 msg 3, "gesture delete: click-drag right button to the bottom"
311 closures[:delete].call(false)
312 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
316 tooltips = Gtk::Tooltips.new
317 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
319 w.signal_connect('key-press-event') { |w,event|
320 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
322 closures[:delete].call(false)
326 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
327 b.signal_connect('clicked') { w.destroy }
330 vb.pack_start(evt, false, false)
331 vb.pack_end(bottom, false, false)
334 w.signal_connect('delete-event') { w.destroy }
335 w.window_position = Gtk::Window::POS_CENTER
339 def scroll_upper(scrolledwindow, ypos_top)
340 newval = scrolledwindow.vadjustment.value -
341 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
342 if newval < scrolledwindow.vadjustment.lower
343 newval = scrolledwindow.vadjustment.lower
345 scrolledwindow.vadjustment.value = newval
348 def scroll_lower(scrolledwindow, ypos_bottom)
349 newval = scrolledwindow.vadjustment.value +
350 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
351 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
352 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
354 scrolledwindow.vadjustment.value = newval
357 def autoscroll_if_needed(scrolledwindow, image, textview)
358 #- autoscroll if cursor or image is not visible, if possible
359 if image && image.window || textview.window
360 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
361 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
362 current_miny_visible = scrolledwindow.vadjustment.value
363 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
364 if ypos_top < current_miny_visible
365 scroll_upper(scrolledwindow, ypos_top)
366 elsif ypos_bottom > current_maxy_visible
367 scroll_lower(scrolledwindow, ypos_bottom)
372 def create_editzone(scrolledwindow, pagenum, image)
373 frame = Gtk::Frame.new
374 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
375 frame.set_shadow_type(Gtk::SHADOW_IN)
376 textview.signal_connect('key-press-event') { |w, event|
377 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
378 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
379 scrolledwindow.signal_emit('key-press-event', event)
381 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
382 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
383 if event.keyval == Gdk::Keyval::GDK_Up
384 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
385 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
387 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
390 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
391 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
393 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
400 candidate_undo_text = nil
401 textview.signal_connect('focus-in-event') { |w, event|
402 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
403 candidate_undo_text = textview.buffer.text
407 textview.signal_connect('key-release-event') { |w, event|
408 if candidate_undo_text && candidate_undo_text != textview.buffer.text
410 save_undo(_("text edit"),
412 save_text = textview.buffer.text
413 textview.buffer.text = text
415 $notebook.set_page(pagenum)
417 textview.buffer.text = save_text
419 $notebook.set_page(pagenum)
421 }, candidate_undo_text)
422 candidate_undo_text = nil
425 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)
426 autoscroll_if_needed(scrolledwindow, image, textview)
431 return [ frame, textview ]
434 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
436 if !$modified_pixbufs[thumbnail_img]
437 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
438 elsif !$modified_pixbufs[thumbnail_img][:orig]
439 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
442 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
445 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
446 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
447 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
448 if pixbuf.height > desired_y
449 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
450 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
451 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
456 if $modified_pixbufs[thumbnail_img][:whitebalance]
457 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
460 #- fix gamma correction
461 if $modified_pixbufs[thumbnail_img][:gammacorrect]
462 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
465 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
468 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
471 #- update rotate attribute
472 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
473 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
475 if $config['rotate-set-exif'] == 'true'
476 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
479 $modified_pixbufs[thumbnail_img] ||= {}
480 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
481 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
483 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
486 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
489 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
491 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
493 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
494 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
496 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
497 $notebook.set_page(0)
498 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
503 def color_swap(xmldir, attributes_prefix)
505 if xmldir.attributes["#{attributes_prefix}color-swap"]
506 xmldir.delete_attribute("#{attributes_prefix}color-swap")
508 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
512 def enhance(xmldir, attributes_prefix)
514 if xmldir.attributes["#{attributes_prefix}enhance"]
515 xmldir.delete_attribute("#{attributes_prefix}enhance")
517 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
521 def change_seektime(xmldir, attributes_prefix, value)
523 xmldir.add_attribute("#{attributes_prefix}seektime", value)
526 def ask_new_seektime(xmldir, attributes_prefix)
528 value = xmldir.attributes["#{attributes_prefix}seektime"]
533 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
535 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
536 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
537 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
541 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
545 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
546 entry.signal_connect('key-press-event') { |w, event|
547 if event.keyval == Gdk::Keyval::GDK_Return
548 dialog.response(Gtk::Dialog::RESPONSE_OK)
550 elsif event.keyval == Gdk::Keyval::GDK_Escape
551 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
554 false #- propagate if needed
558 dialog.window_position = Gtk::Window::POS_MOUSE
561 dialog.run { |response|
564 if response == Gtk::Dialog::RESPONSE_OK
566 msg 3, "changing seektime to #{newval}"
567 return { :old => value, :new => newval }
574 def change_pano_amount(xmldir, attributes_prefix, value)
577 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
579 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
583 def ask_new_pano_amount(xmldir, attributes_prefix)
585 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
590 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
592 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
593 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
594 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
598 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
599 of this panorama image compared to other regular images. For example, if the panorama
600 was taken out of four photos on one row, counting the necessary overlap, the width of
601 this panorama image should probably be roughly three times the width of regular images.
603 With this information, booh will be able to generate panorama thumbnails looking
604 the right 'size', since the height of the thumbnail for this image will be similar
605 to the height of other thumbnails.
608 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)")))).
609 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
610 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
611 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
612 spin.signal_connect('value-changed') {
615 dialog.window_position = Gtk::Window::POS_MOUSE
618 spin.value = value.to_f
625 dialog.run { |response|
629 newval = spin.value.to_f
632 if response == Gtk::Dialog::RESPONSE_OK
634 msg 3, "changing panorama amount to #{newval}"
635 return { :old => value, :new => newval }
642 def change_whitebalance(xmlelem, attributes_prefix, value)
644 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
647 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
649 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
650 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
651 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
652 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
653 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
654 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
655 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
656 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
657 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
658 $modified_pixbufs[thumbnail_img] ||= {}
659 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
660 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
662 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
663 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
665 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
668 $modified_pixbufs[thumbnail_img] ||= {}
669 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
671 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
674 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
675 #- init $modified_pixbufs correctly
676 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
678 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
680 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
682 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
683 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
684 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
688 _("You can fix the <b>white balance</b> of the image, if your image is too blue
689 or too yellow because the recorder didn't detect the light correctly. Drag the
690 slider below the image to the left for more blue, to the right for more yellow.
694 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
696 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
698 dialog.window_position = Gtk::Window::POS_MOUSE
702 timeout = Gtk.timeout_add(100) {
703 if hs.value != lastval
706 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
712 dialog.run { |response|
713 Gtk.timeout_remove(timeout)
714 if response == Gtk::Dialog::RESPONSE_OK
716 newval = hs.value.to_s
717 msg 3, "changing white balance to #{newval}"
719 return { :old => value, :new => newval }
722 $modified_pixbufs[thumbnail_img] ||= {}
723 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
724 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
732 def change_gammacorrect(xmlelem, attributes_prefix, value)
734 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
737 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
739 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
740 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
741 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
742 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
743 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
744 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
745 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
746 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
747 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
748 $modified_pixbufs[thumbnail_img] ||= {}
749 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
750 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
752 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
753 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
755 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
758 $modified_pixbufs[thumbnail_img] ||= {}
759 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
761 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
764 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
765 #- init $modified_pixbufs correctly
766 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
768 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
770 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
772 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
773 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
774 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
778 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
779 or too bright. Drag the slider below the image.
783 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
785 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
787 dialog.window_position = Gtk::Window::POS_MOUSE
791 timeout = Gtk.timeout_add(100) {
792 if hs.value != lastval
795 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
801 dialog.run { |response|
802 Gtk.timeout_remove(timeout)
803 if response == Gtk::Dialog::RESPONSE_OK
805 newval = hs.value.to_s
806 msg 3, "gamma correction to #{newval}"
808 return { :old => value, :new => newval }
811 $modified_pixbufs[thumbnail_img] ||= {}
812 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
813 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
821 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
822 if File.exists?(destfile)
823 File.delete(destfile)
825 #- type can be 'element' or 'subdir'
827 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
829 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
833 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
835 push_mousecursor_wait
836 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
839 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
845 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
846 distribute_multiple_call = Proc.new { |action, arg|
847 $selected_elements.each_key { |path|
848 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
850 if possible_actions[:can_multiple] && $selected_elements.length > 0
851 UndoHandler.begin_batch
852 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
853 UndoHandler.end_batch
855 closures[action].call(arg)
857 $selected_elements = {}
860 if optionals.include?('change_image')
861 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
862 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
863 changeimg.signal_connect('activate') { closures[:change].call }
864 menu.append(Gtk::SeparatorMenuItem.new)
866 if !possible_actions[:can_multiple] || $selected_elements.length == 0
869 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
870 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
871 view.signal_connect('activate') { closures[:view].call }
873 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
874 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
875 view.signal_connect('activate') { closures[:view].call }
876 menu.append(Gtk::SeparatorMenuItem.new)
879 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
880 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
881 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
882 exif.signal_connect('activate') { show_popup($main_window,
883 utf8(`exif -m '#{fullpath}'`),
884 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
885 menu.append(Gtk::SeparatorMenuItem.new)
888 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
889 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
890 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
891 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
892 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
893 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
894 if !possible_actions[:can_multiple] || $selected_elements.length == 0
895 menu.append(Gtk::SeparatorMenuItem.new)
896 if !possible_actions[:forbid_left]
897 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
898 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
899 moveleft.signal_connect('activate') { closures[:move].call('left') }
900 if !possible_actions[:can_left]
901 moveleft.sensitive = false
904 if !possible_actions[:forbid_right]
905 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
906 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
907 moveright.signal_connect('activate') { closures[:move].call('right') }
908 if !possible_actions[:can_right]
909 moveright.sensitive = false
912 if optionals.include?('move_top')
913 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
914 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
915 movetop.signal_connect('activate') { closures[:move].call('top') }
916 if !possible_actions[:can_top]
917 movetop.sensitive = false
920 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
921 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
922 moveup.signal_connect('activate') { closures[:move].call('up') }
923 if !possible_actions[:can_up]
924 moveup.sensitive = false
926 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
927 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
928 movedown.signal_connect('activate') { closures[:move].call('down') }
929 if !possible_actions[:can_down]
930 movedown.sensitive = false
932 if optionals.include?('move_bottom')
933 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
934 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
935 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
936 if !possible_actions[:can_bottom]
937 movebottom.sensitive = false
942 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
943 menu.append(Gtk::SeparatorMenuItem.new)
944 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
945 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
946 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
947 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
948 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
949 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
950 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
951 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
952 seektime.signal_connect('activate') {
953 if possible_actions[:can_multiple] && $selected_elements.length > 0
954 if values = ask_new_seektime(nil, '')
955 distribute_multiple_call.call(:seektime, values)
958 closures[:seektime].call
963 menu.append( Gtk::SeparatorMenuItem.new)
964 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
965 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
966 gammacorrect.signal_connect('activate') {
967 if possible_actions[:can_multiple] && $selected_elements.length > 0
968 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
969 distribute_multiple_call.call(:gammacorrect, values)
972 closures[:gammacorrect].call
975 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
976 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
977 whitebalance.signal_connect('activate') {
978 if possible_actions[:can_multiple] && $selected_elements.length > 0
979 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
980 distribute_multiple_call.call(:whitebalance, values)
983 closures[:whitebalance].call
986 if !possible_actions[:can_multiple] || $selected_elements.length == 0
987 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
988 _("Enhance constrast"))))
990 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
992 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
993 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
994 if type == 'image' && possible_actions[:can_panorama]
995 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
996 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
997 panorama.signal_connect('activate') {
998 if possible_actions[:can_multiple] && $selected_elements.length > 0
999 if values = ask_new_pano_amount(nil, '')
1000 distribute_multiple_call.call(:pano, values)
1003 distribute_multiple_call.call(:pano)
1007 menu.append( Gtk::SeparatorMenuItem.new)
1008 if optionals.include?('delete')
1009 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1010 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1011 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1012 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1013 paste_item.signal_connect('activate') { closures[:paste].call }
1014 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1015 clear_item.signal_connect('activate') { $cuts = [] }
1017 paste_item.sensitive = clear_item.sensitive = false
1020 menu.append( Gtk::SeparatorMenuItem.new)
1022 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1023 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1024 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1025 editexternally.signal_connect('activate') {
1026 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1031 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1032 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1033 if optionals.include?('delete')
1034 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1035 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1038 menu.popup(nil, nil, event.button, event.time)
1041 def delete_current_subalbum
1043 sel = $albums_tv.selection.selected_rows
1044 $xmldir.elements.each { |e|
1045 if e.name == 'image' || e.name == 'video'
1046 e.add_attribute('deleted', 'true')
1049 #- branch if we have a non deleted subalbum
1050 if $xmldir.child_byname_notattr('dir', 'deleted')
1051 $xmldir.delete_attribute('thumbnails-caption')
1052 $xmldir.delete_attribute('thumbnails-captionfile')
1054 $xmldir.add_attribute('deleted', 'true')
1056 while moveup.parent.name == 'dir'
1057 moveup = moveup.parent
1058 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1059 moveup.add_attribute('deleted', 'true')
1066 save_changes('forced')
1067 populate_subalbums_treeview(false)
1068 $albums_tv.selection.select_path(sel[0])
1074 $current_path = nil #- prevent save_changes from being rerun again
1075 sel = $albums_tv.selection.selected_rows
1076 restore_one = proc { |xmldir|
1077 xmldir.elements.each { |e|
1078 if e.name == 'dir' && e.attributes['deleted']
1081 e.delete_attribute('deleted')
1084 restore_one.call($xmldir)
1085 populate_subalbums_treeview(false)
1086 $albums_tv.selection.select_path(sel[0])
1089 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1092 frame1 = Gtk::Frame.new
1093 fullpath = from_utf8("#{$current_path}/#{filename}")
1095 my_gen_real_thumbnail = proc {
1096 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1100 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1101 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1102 pack_start(img = Gtk::Image.new).
1103 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1104 px, mask = pxb.render_pixmap_and_mask(0)
1105 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1106 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1108 frame1.add(img = Gtk::Image.new)
1111 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1112 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1113 my_gen_real_thumbnail.call
1115 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1118 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1120 tooltips = Gtk::Tooltips.new
1121 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1122 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1124 frame2, textview = create_editzone($autotable_sw, 1, img)
1125 textview.buffer.text = caption
1126 textview.set_justification(Gtk::Justification::CENTER)
1128 vbox = Gtk::VBox.new(false, 5)
1129 vbox.pack_start(evtbox, false, false)
1130 vbox.pack_start(frame2, false, false)
1131 autotable.append(vbox, filename)
1133 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1134 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1136 #- to be able to find widgets by name
1137 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1139 cleanup_all_thumbnails = proc {
1140 #- remove out of sync images
1141 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1142 for sizeobj in $images_size
1143 #- cannot use sizeobj because panoramic images will have a larger width
1144 Dir.glob("#{dest_img_base}-*.jpg") do |file|
1152 cleanup_all_thumbnails.call
1153 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1155 $xmldir.delete_attribute('already-generated')
1156 my_gen_real_thumbnail.call
1159 rotate_and_cleanup = proc { |angle|
1160 cleanup_all_thumbnails.call
1161 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1164 move = proc { |direction|
1165 do_method = "move_#{direction}"
1166 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1168 done = autotable.method(do_method).call(vbox)
1169 textview.grab_focus #- because if moving, focus is stolen
1173 save_undo(_("move %s") % direction,
1175 autotable.method(undo_method).call(vbox)
1176 textview.grab_focus #- because if moving, focus is stolen
1177 autoscroll_if_needed($autotable_sw, img, textview)
1178 $notebook.set_page(1)
1180 autotable.method(do_method).call(vbox)
1181 textview.grab_focus #- because if moving, focus is stolen
1182 autoscroll_if_needed($autotable_sw, img, textview)
1183 $notebook.set_page(1)
1189 color_swap_and_cleanup = proc {
1190 perform_color_swap_and_cleanup = proc {
1191 cleanup_all_thumbnails.call
1192 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1193 my_gen_real_thumbnail.call
1196 perform_color_swap_and_cleanup.call
1198 save_undo(_("color swap"),
1200 perform_color_swap_and_cleanup.call
1202 autoscroll_if_needed($autotable_sw, img, textview)
1203 $notebook.set_page(1)
1205 perform_color_swap_and_cleanup.call
1207 autoscroll_if_needed($autotable_sw, img, textview)
1208 $notebook.set_page(1)
1213 change_seektime_and_cleanup_real = proc { |values|
1214 perform_change_seektime_and_cleanup = proc { |val|
1215 cleanup_all_thumbnails.call
1216 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1217 my_gen_real_thumbnail.call
1219 perform_change_seektime_and_cleanup.call(values[:new])
1221 save_undo(_("specify seektime"),
1223 perform_change_seektime_and_cleanup.call(values[:old])
1225 autoscroll_if_needed($autotable_sw, img, textview)
1226 $notebook.set_page(1)
1228 perform_change_seektime_and_cleanup.call(values[:new])
1230 autoscroll_if_needed($autotable_sw, img, textview)
1231 $notebook.set_page(1)
1236 change_seektime_and_cleanup = proc {
1237 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1238 change_seektime_and_cleanup_real.call(values)
1242 change_pano_amount_and_cleanup_real = proc { |values|
1243 perform_change_pano_amount_and_cleanup = proc { |val|
1244 cleanup_all_thumbnails.call
1245 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1247 perform_change_pano_amount_and_cleanup.call(values[:new])
1249 save_undo(_("change panorama amount"),
1251 perform_change_pano_amount_and_cleanup.call(values[:old])
1253 autoscroll_if_needed($autotable_sw, img, textview)
1254 $notebook.set_page(1)
1256 perform_change_pano_amount_and_cleanup.call(values[:new])
1258 autoscroll_if_needed($autotable_sw, img, textview)
1259 $notebook.set_page(1)
1264 change_pano_amount_and_cleanup = proc {
1265 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1266 change_pano_amount_and_cleanup_real.call(values)
1270 whitebalance_and_cleanup_real = proc { |values|
1271 perform_change_whitebalance_and_cleanup = proc { |val|
1272 cleanup_all_thumbnails.call
1273 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1274 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1275 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1277 perform_change_whitebalance_and_cleanup.call(values[:new])
1279 save_undo(_("fix white balance"),
1281 perform_change_whitebalance_and_cleanup.call(values[:old])
1283 autoscroll_if_needed($autotable_sw, img, textview)
1284 $notebook.set_page(1)
1286 perform_change_whitebalance_and_cleanup.call(values[:new])
1288 autoscroll_if_needed($autotable_sw, img, textview)
1289 $notebook.set_page(1)
1294 whitebalance_and_cleanup = proc {
1295 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1296 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1297 whitebalance_and_cleanup_real.call(values)
1301 gammacorrect_and_cleanup_real = proc { |values|
1302 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1303 cleanup_all_thumbnails.call
1304 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1305 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1306 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1308 perform_change_gammacorrect_and_cleanup.call(values[:new])
1310 save_undo(_("gamma correction"),
1312 perform_change_gammacorrect_and_cleanup.call(values[:old])
1314 autoscroll_if_needed($autotable_sw, img, textview)
1315 $notebook.set_page(1)
1317 perform_change_gammacorrect_and_cleanup.call(values[:new])
1319 autoscroll_if_needed($autotable_sw, img, textview)
1320 $notebook.set_page(1)
1325 gammacorrect_and_cleanup = Proc.new {
1326 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1327 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1328 gammacorrect_and_cleanup_real.call(values)
1332 enhance_and_cleanup = proc {
1333 perform_enhance_and_cleanup = proc {
1334 cleanup_all_thumbnails.call
1335 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1336 my_gen_real_thumbnail.call
1339 cleanup_all_thumbnails.call
1340 perform_enhance_and_cleanup.call
1342 save_undo(_("enhance"),
1344 perform_enhance_and_cleanup.call
1346 autoscroll_if_needed($autotable_sw, img, textview)
1347 $notebook.set_page(1)
1349 perform_enhance_and_cleanup.call
1351 autoscroll_if_needed($autotable_sw, img, textview)
1352 $notebook.set_page(1)
1357 delete = proc { |isacut|
1358 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 })
1361 perform_delete = proc {
1362 after = autotable.get_next_widget(vbox)
1364 after = autotable.get_previous_widget(vbox)
1366 if $config['deleteondisk'] && !isacut
1367 msg 3, "scheduling for delete: #{fullpath}"
1368 $todelete << fullpath
1370 autotable.remove_widget(vbox)
1372 $vbox2widgets[after][:textview].grab_focus
1373 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1377 previous_pos = autotable.get_current_number(vbox)
1381 delete_current_subalbum
1383 save_undo(_("delete"),
1385 autotable.reinsert(pos, vbox, filename)
1386 $notebook.set_page(1)
1387 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1389 msg 3, "removing deletion schedule of: #{fullpath}"
1390 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1393 $notebook.set_page(1)
1402 $cuts << { :vbox => vbox, :filename => filename }
1403 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1408 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1411 autotable.queue_draws << proc {
1412 $vbox2widgets[last[:vbox]][:textview].grab_focus
1413 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1415 save_undo(_("paste"),
1417 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1418 $notebook.set_page(1)
1421 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1423 $notebook.set_page(1)
1426 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1431 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1432 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1433 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1434 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1436 textview.signal_connect('key-press-event') { |w, event|
1439 x, y = autotable.get_current_pos(vbox)
1440 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1441 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1442 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1443 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1445 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1446 $vbox2widgets[widget_up][:textview].grab_focus
1453 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1455 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1456 $vbox2widgets[widget_down][:textview].grab_focus
1463 if event.keyval == Gdk::Keyval::GDK_Left
1466 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1473 rotate_and_cleanup.call(-90)
1476 if event.keyval == Gdk::Keyval::GDK_Right
1477 next_ = autotable.get_next_widget(vbox)
1478 if next_ && autotable.get_current_pos(next_)[0] > x
1480 $vbox2widgets[next_][:textview].grab_focus
1487 rotate_and_cleanup.call(90)
1490 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1493 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1494 view_element(filename, { :delete => delete })
1497 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1500 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1504 !propagate #- propagate if needed
1507 $ignore_next_release = false
1508 evtbox.signal_connect('button-press-event') { |w, event|
1509 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1510 if event.state & Gdk::Window::BUTTON3_MASK != 0
1511 #- gesture redo: hold right mouse button then click left mouse button
1512 $config['nogestures'] or perform_redo
1513 $ignore_next_release = true
1515 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1517 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1519 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1520 elsif $enhance.active?
1521 enhance_and_cleanup.call
1522 elsif $delete.active?
1526 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1529 $button1_pressed_autotable = true
1530 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1531 if event.state & Gdk::Window::BUTTON1_MASK != 0
1532 #- gesture undo: hold left mouse button then click right mouse button
1533 $config['nogestures'] or perform_undo
1534 $ignore_next_release = true
1536 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1537 view_element(filename, { :delete => delete })
1542 evtbox.signal_connect('button-release-event') { |w, event|
1543 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1544 if !$ignore_next_release
1545 x, y = autotable.get_current_pos(vbox)
1546 next_ = autotable.get_next_widget(vbox)
1547 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1548 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1549 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1550 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1551 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1552 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1553 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1555 $ignore_next_release = false
1556 $gesture_press = nil
1561 #- handle reordering with drag and drop
1562 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1563 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1564 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1565 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1568 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1570 #- mouse gesture first (dnd disables button-release-event)
1571 if $gesture_press && $gesture_press[:filename] == filename
1572 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1573 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1574 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1575 rotate_and_cleanup.call(angle)
1576 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1578 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1579 msg 3, "gesture delete: click-drag right button to the bottom"
1581 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1586 ctxt.targets.each { |target|
1587 if target.name == 'reorder-elements'
1588 move_dnd = proc { |from,to|
1591 autotable.move(from, to)
1592 save_undo(_("reorder"),
1595 autotable.move(to - 1, from)
1597 autotable.move(to, from + 1)
1599 $notebook.set_page(1)
1601 autotable.move(from, to)
1602 $notebook.set_page(1)
1607 if $multiple_dnd.size == 0
1608 move_dnd.call(selection_data.data.to_i,
1609 autotable.get_current_number(vbox))
1611 UndoHandler.begin_batch
1612 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1614 #- need to update current position between each call
1615 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1616 autotable.get_current_number(vbox))
1618 UndoHandler.end_batch
1629 def create_auto_table
1631 $autotable = Gtk::AutoTable.new(5)
1633 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1634 thumbnails_vb = Gtk::VBox.new(false, 5)
1636 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1637 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1638 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1639 thumbnails_vb.add($autotable)
1641 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1642 $autotable_sw.add_with_viewport(thumbnails_vb)
1644 #- follows stuff for handling multiple elements selection
1645 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1647 update_selected = proc {
1648 $autotable.current_order.each { |path|
1649 w = $name2widgets[path][:evtbox].window
1650 xm = w.position[0] + w.size[0]/2
1651 ym = w.position[1] + w.size[1]/2
1652 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1653 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1654 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1655 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1658 if $selected_elements[path] && ! $selected_elements[path][:keep]
1659 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))
1660 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1661 $selected_elements.delete(path)
1666 $autotable.signal_connect('realize') { |w,e|
1667 gc = Gdk::GC.new($autotable.window)
1668 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1669 gc.function = Gdk::GC::INVERT
1670 #- autoscroll handling for DND and multiple selections
1671 Gtk.timeout_add(100) {
1672 if ! $autotable.window.nil?
1673 w, x, y, mask = $autotable.window.pointer
1674 if mask & Gdk::Window::BUTTON1_MASK != 0
1675 if y < $autotable_sw.vadjustment.value
1677 $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]])
1679 if $button1_pressed_autotable || press_x
1680 scroll_upper($autotable_sw, y)
1683 w, pos_x, pos_y = $autotable.window.pointer
1684 $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]])
1685 update_selected.call
1688 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1690 $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]])
1692 if $button1_pressed_autotable || press_x
1693 scroll_lower($autotable_sw, y)
1696 w, pos_x, pos_y = $autotable.window.pointer
1697 $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]])
1698 update_selected.call
1703 ! $autotable.window.nil?
1707 $autotable.signal_connect('button-press-event') { |w,e|
1709 if !$button1_pressed_autotable
1712 if e.state & Gdk::Window::SHIFT_MASK == 0
1713 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1714 $selected_elements = {}
1715 $statusbar.push(0, utf8(_("Nothing selected.")))
1717 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1719 set_mousecursor(Gdk::Cursor::TCROSS)
1723 $autotable.signal_connect('button-release-event') { |w,e|
1725 if $button1_pressed_autotable
1726 #- unselect all only now
1727 $multiple_dnd = $selected_elements.keys
1728 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1729 $selected_elements = {}
1730 $button1_pressed_autotable = false
1733 $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]])
1734 if $selected_elements.length > 0
1735 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1738 press_x = press_y = pos_x = pos_y = nil
1739 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1743 $autotable.signal_connect('motion-notify-event') { |w,e|
1746 $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]])
1750 $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]])
1751 update_selected.call
1757 def create_subalbums_page
1759 subalbums_hb = Gtk::HBox.new
1760 $subalbums_vb = Gtk::VBox.new(false, 5)
1761 subalbums_hb.pack_start($subalbums_vb, false, false)
1762 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1763 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1764 $subalbums_sw.add_with_viewport(subalbums_hb)
1767 def save_current_file
1773 ios = File.open($filename, "w")
1774 $xmldoc.write(ios, 0)
1776 rescue Iconv::IllegalSequence
1777 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1778 if ! ios.nil? && ! ios.closed?
1781 $xmldoc.xml_decl.encoding = 'UTF-8'
1782 ios = File.open($filename, "w")
1783 $xmldoc.write(ios, 0)
1794 def save_current_file_user
1795 save_tempfilename = $filename
1796 $filename = $orig_filename
1797 if ! save_current_file
1798 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1799 $filename = save_tempfilename
1803 $generated_outofline = false
1804 $filename = save_tempfilename
1806 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1807 $todelete.each { |f|
1812 def mark_document_as_dirty
1813 $xmldoc.elements.each('//dir') { |elem|
1814 elem.delete_attribute('already-generated')
1818 #- ret: true => ok false => cancel
1819 def ask_save_modifications(msg1, msg2, *options)
1821 options = options.size > 0 ? options[0] : {}
1823 if options[:disallow_cancel]
1824 dialog = Gtk::Dialog.new(msg1,
1826 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1827 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1828 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1830 dialog = Gtk::Dialog.new(msg1,
1832 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1833 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1834 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1835 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1837 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1838 dialog.vbox.add(Gtk::Label.new(msg2))
1839 dialog.window_position = Gtk::Window::POS_CENTER
1842 dialog.run { |response|
1844 if response == Gtk::Dialog::RESPONSE_YES
1845 if ! save_current_file_user
1846 return ask_save_modifications(msg1, msg2, options)
1849 #- if we have generated an album but won't save modifications, we must remove
1850 #- already-generated markers in original file
1851 if $generated_outofline
1853 $xmldoc = REXML::Document.new File.new($orig_filename)
1854 mark_document_as_dirty
1855 ios = File.open($orig_filename, "w")
1856 $xmldoc.write(ios, 0)
1859 puts "exception: #{$!}"
1863 if response == Gtk::Dialog::RESPONSE_CANCEL
1866 $todelete = [] #- unconditionally clear the list of images/videos to delete
1872 def try_quit(*options)
1873 if ask_save_modifications(utf8(_("Save before quitting?")),
1874 utf8(_("Do you want to save your changes before quitting?")),
1880 def show_popup(parent, msg, *options)
1881 dialog = Gtk::Dialog.new
1882 if options[0] && options[0][:title]
1883 dialog.title = options[0][:title]
1885 dialog.title = utf8(_("Booh message"))
1887 lbl = Gtk::Label.new
1888 if options[0] && options[0][:nomarkup]
1893 if options[0] && options[0][:centered]
1894 lbl.set_justify(Gtk::Justification::CENTER)
1896 if options[0] && options[0][:selectable]
1897 lbl.selectable = true
1899 if options[0] && options[0][:topwidget]
1900 dialog.vbox.add(options[0][:topwidget])
1902 if options[0] && options[0][:scrolled]
1903 sw = Gtk::ScrolledWindow.new(nil, nil)
1904 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1905 sw.add_with_viewport(lbl)
1907 dialog.set_default_size(500, 600)
1909 dialog.vbox.add(lbl)
1910 dialog.set_default_size(200, 120)
1912 if options[0] && options[0][:okcancel]
1913 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1915 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1917 if options[0] && options[0][:pos_centered]
1918 dialog.window_position = Gtk::Window::POS_CENTER
1920 dialog.window_position = Gtk::Window::POS_MOUSE
1923 if options[0] && options[0][:linkurl]
1924 linkbut = Gtk::Button.new('')
1925 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1926 linkbut.signal_connect('clicked') {
1927 open_url(options[0][:linkurl])
1928 dialog.response(Gtk::Dialog::RESPONSE_OK)
1929 set_mousecursor_normal
1931 linkbut.relief = Gtk::RELIEF_NONE
1932 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1933 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1934 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1939 if !options[0] || !options[0][:not_transient]
1940 dialog.transient_for = parent
1941 dialog.run { |response|
1943 if options[0] && options[0][:okcancel]
1944 return response == Gtk::Dialog::RESPONSE_OK
1948 dialog.signal_connect('response') { dialog.destroy }
1952 def set_mainwindow_title(progress)
1953 filename = $orig_filename || $filename
1956 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
1958 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
1962 $main_window.title = 'booh - ' + File.basename(filename)
1964 $main_window.title = 'booh'
1969 def backend_wait_message(parent, msg, infopipe_path, mode)
1971 w.set_transient_for(parent)
1974 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1975 vb.pack_start(Gtk::Label.new(msg), false, false)
1977 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1978 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
1979 if mode != 'one dir scan'
1980 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1982 if mode == 'web-album'
1983 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1984 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1986 vb.pack_start(Gtk::HSeparator.new, false, false)
1988 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1989 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1990 vb.pack_end(bottom, false, false)
1993 update_progression_title_pb1 = proc {
1994 if mode == 'web-album'
1995 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
1996 elsif mode != 'one dir scan'
1997 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
1999 set_mainwindow_title(pb1_1.fraction)
2003 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2004 refresh_thread = Thread.new {
2005 directories_counter = 0
2006 while line = infopipe.gets
2007 if line =~ /^directories: (\d+), sizes: (\d+)/
2008 directories = $1.to_f + 1
2010 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2011 elements = $3.to_f + 1
2012 if mode == 'web-album'
2016 gtk_thread_protect { pb1_1.fraction = 0 }
2017 if mode != 'one dir scan'
2018 newtext = utf8(full_src_dir_to_rel($1, $2))
2019 newtext = '/' if newtext == ''
2020 gtk_thread_protect { pb1_2.text = newtext }
2021 directories_counter += 1
2022 gtk_thread_protect {
2023 pb1_2.fraction = directories_counter / directories
2024 update_progression_title_pb1.call
2027 elsif line =~ /^processing element$/
2028 element_counter += 1
2029 gtk_thread_protect {
2030 pb1_1.fraction = element_counter / elements
2031 update_progression_title_pb1.call
2033 elsif line =~ /^processing size$/
2034 element_counter += 1
2035 gtk_thread_protect {
2036 pb1_1.fraction = element_counter / elements
2037 update_progression_title_pb1.call
2039 elsif line =~ /^finished processing sizes$/
2040 gtk_thread_protect { pb1_1.fraction = 1 }
2041 elsif line =~ /^creating index.html$/
2042 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2043 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2044 directories_counter = 0
2045 elsif line =~ /^index.html: (.+)\|(.+)/
2046 newtext = utf8(full_src_dir_to_rel($1, $2))
2047 newtext = '/' if newtext == ''
2048 gtk_thread_protect { pb2.text = newtext }
2049 directories_counter += 1
2050 gtk_thread_protect {
2051 pb2.fraction = directories_counter / directories
2052 set_mainwindow_title(0.9 + pb2.fraction / 10)
2054 elsif line =~ /^die: (.*)$/
2061 w.signal_connect('delete-event') { w.destroy }
2062 w.signal_connect('destroy') {
2063 Thread.kill(refresh_thread)
2064 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2067 File.delete(infopipe_path)
2069 set_mainwindow_title(nil)
2071 w.window_position = Gtk::Window::POS_CENTER
2077 def call_backend(cmd, waitmsg, mode, params)
2078 pipe = Tempfile.new("boohpipe")
2079 Thread.critical = true
2082 system("mkfifo #{path}")
2083 Thread.critical = false
2084 cmd += " --info-pipe #{path}"
2085 button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2090 id, exitstatus = Process.waitpid2(pid)
2091 gtk_thread_protect { w8.destroy }
2093 if params[:successmsg]
2094 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2096 if params[:closure_after]
2097 gtk_thread_protect(¶ms[:closure_after])
2099 elsif exitstatus == 15
2100 #- say nothing, user aborted
2102 gtk_thread_protect { show_popup($main_window,
2103 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2109 button.signal_connect('clicked') {
2110 Process.kill('SIGTERM', pid)
2114 def save_changes(*forced)
2115 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2119 $xmldir.delete_attribute('already-generated')
2121 propagate_children = proc { |xmldir|
2122 if xmldir.attributes['subdirs-caption']
2123 xmldir.delete_attribute('already-generated')
2125 xmldir.elements.each('dir') { |element|
2126 propagate_children.call(element)
2130 if $xmldir.child_byname_notattr('dir', 'deleted')
2131 new_title = $subalbums_title.buffer.text
2132 if new_title != $xmldir.attributes['subdirs-caption']
2133 parent = $xmldir.parent
2134 if parent.name == 'dir'
2135 parent.delete_attribute('already-generated')
2137 propagate_children.call($xmldir)
2139 $xmldir.add_attribute('subdirs-caption', new_title)
2140 $xmldir.elements.each('dir') { |element|
2141 if !element.attributes['deleted']
2142 path = element.attributes['path']
2143 newtext = $subalbums_edits[path][:editzone].buffer.text
2144 if element.attributes['subdirs-caption']
2145 if element.attributes['subdirs-caption'] != newtext
2146 propagate_children.call(element)
2148 element.add_attribute('subdirs-caption', newtext)
2149 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2151 if element.attributes['thumbnails-caption'] != newtext
2152 element.delete_attribute('already-generated')
2154 element.add_attribute('thumbnails-caption', newtext)
2155 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2161 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2162 if $xmldir.attributes['thumbnails-caption']
2163 path = $xmldir.attributes['path']
2164 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2166 elsif $xmldir.attributes['thumbnails-caption']
2167 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2170 if $xmldir.attributes['thumbnails-caption']
2171 if edit = $subalbums_edits[$xmldir.attributes['path']]
2172 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2176 #- remove and reinsert elements to reflect new ordering
2179 $xmldir.elements.each { |element|
2180 if element.name == 'image' || element.name == 'video'
2181 saves[element.attributes['filename']] = element.remove
2185 $autotable.current_order.each { |path|
2186 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2187 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2190 saves.each_key { |path|
2191 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2192 chld.add_attribute('deleted', 'true')
2196 def sort_by_exif_date
2200 $xmldir.elements.each { |element|
2201 if element.name == 'image' || element.name == 'video'
2202 current_order << element.attributes['filename']
2206 #- look for EXIF dates
2209 if current_order.size > 20
2211 w.set_transient_for($main_window)
2213 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2214 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2215 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2216 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2217 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2218 vb.pack_end(bottom, false, false)
2220 w.signal_connect('delete-event') { w.destroy }
2221 w.window_position = Gtk::Window::POS_CENTER
2225 b.signal_connect('clicked') { aborted = true }
2227 current_order.each { |f|
2229 if entry2type(f) == 'image'
2231 pb.fraction = i.to_f / current_order.size
2232 Gtk.main_iteration while Gtk.events_pending?
2233 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2235 dates[f] = date_time
2248 current_order.each { |f|
2249 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2251 dates[f] = date_time
2257 $xmldir.elements.each { |element|
2258 if element.name == 'image' || element.name == 'video'
2259 saves[element.attributes['filename']] = element.remove
2263 neworder = smartsort(current_order, dates)
2266 $xmldir.add_element(saves[f].name, saves[f].attributes)
2269 #- let the auto-table reflect new ordering
2273 def remove_all_captions
2276 $autotable.current_order.each { |path|
2277 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2278 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2280 save_undo(_("remove all captions"),
2282 texts.each_key { |key|
2283 $name2widgets[key][:textview].buffer.text = texts[key]
2285 $notebook.set_page(1)
2287 texts.each_key { |key|
2288 $name2widgets[key][:textview].buffer.text = ''
2290 $notebook.set_page(1)
2296 $selected_elements.each_key { |path|
2297 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2303 $selected_elements = {}
2307 $undo_tb.sensitive = $undo_mb.sensitive = false
2308 $redo_tb.sensitive = $redo_mb.sensitive = false
2314 $subalbums_vb.children.each { |chld|
2315 $subalbums_vb.remove(chld)
2317 $subalbums = Gtk::Table.new(0, 0, true)
2318 current_y_sub_albums = 0
2320 $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"])
2321 $subalbums_edits = {}
2322 subalbums_counter = 0
2323 subalbums_edits_bypos = {}
2325 add_subalbum = proc { |xmldir, counter|
2326 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2327 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2328 if xmldir == $xmldir
2329 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2330 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2331 caption = xmldir.attributes['thumbnails-caption']
2332 infotype = 'thumbnails'
2334 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2335 captionfile, caption = find_subalbum_caption_info(xmldir)
2336 infotype = find_subalbum_info_type(xmldir)
2338 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2339 hbox = Gtk::HBox.new
2340 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2342 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2345 my_gen_real_thumbnail = proc {
2346 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2349 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2350 f.add(img = Gtk::Image.new)
2351 my_gen_real_thumbnail.call
2353 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2355 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2356 $subalbums.attach(hbox,
2357 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2359 frame, textview = create_editzone($subalbums_sw, 0, img)
2360 textview.buffer.text = caption
2361 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2362 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2364 change_image = proc {
2365 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2367 Gtk::FileChooser::ACTION_OPEN,
2369 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2370 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2371 fc.transient_for = $main_window
2372 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))
2373 f.add(preview_img = Gtk::Image.new)
2375 fc.signal_connect('update-preview') { |w|
2377 if fc.preview_filename
2378 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2379 fc.preview_widget_active = true
2381 rescue Gdk::PixbufError
2382 fc.preview_widget_active = false
2385 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2387 old_file = captionfile
2388 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2389 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2390 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2391 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2393 new_file = fc.filename
2394 msg 3, "new captionfile is: #{fc.filename}"
2395 perform_changefile = proc {
2396 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2397 $modified_pixbufs.delete(thumbnail_file)
2398 xmldir.delete_attribute("#{infotype}-rotate")
2399 xmldir.delete_attribute("#{infotype}-color-swap")
2400 xmldir.delete_attribute("#{infotype}-enhance")
2401 xmldir.delete_attribute("#{infotype}-seektime")
2402 my_gen_real_thumbnail.call
2404 perform_changefile.call
2406 save_undo(_("change caption file for sub-album"),
2408 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2409 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2410 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2411 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2412 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2413 my_gen_real_thumbnail.call
2414 $notebook.set_page(0)
2416 perform_changefile.call
2417 $notebook.set_page(0)
2425 if File.exists?(thumbnail_file)
2426 File.delete(thumbnail_file)
2428 my_gen_real_thumbnail.call
2431 rotate_and_cleanup = proc { |angle|
2432 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2433 if File.exists?(thumbnail_file)
2434 File.delete(thumbnail_file)
2438 move = proc { |direction|
2441 save_changes('forced')
2442 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2443 if direction == 'up'
2444 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2445 subalbums_edits_bypos[oldpos - 1][:position] += 1
2447 if direction == 'down'
2448 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2449 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2451 if direction == 'top'
2452 for i in 1 .. oldpos - 1
2453 subalbums_edits_bypos[i][:position] += 1
2455 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2457 if direction == 'bottom'
2458 for i in oldpos + 1 .. subalbums_counter
2459 subalbums_edits_bypos[i][:position] -= 1
2461 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2465 $xmldir.elements.each('dir') { |element|
2466 if (!element.attributes['deleted'])
2467 elems << [ element.attributes['path'], element.remove ]
2470 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2471 each { |e| $xmldir.add_element(e[1]) }
2472 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2473 $xmldir.elements.each('descendant::dir') { |elem|
2474 elem.delete_attribute('already-generated')
2477 sel = $albums_tv.selection.selected_rows
2479 populate_subalbums_treeview(false)
2480 $albums_tv.selection.select_path(sel[0])
2483 color_swap_and_cleanup = proc {
2484 perform_color_swap_and_cleanup = proc {
2485 color_swap(xmldir, "#{infotype}-")
2486 my_gen_real_thumbnail.call
2488 perform_color_swap_and_cleanup.call
2490 save_undo(_("color swap"),
2492 perform_color_swap_and_cleanup.call
2493 $notebook.set_page(0)
2495 perform_color_swap_and_cleanup.call
2496 $notebook.set_page(0)
2501 change_seektime_and_cleanup = proc {
2502 if values = ask_new_seektime(xmldir, "#{infotype}-")
2503 perform_change_seektime_and_cleanup = proc { |val|
2504 change_seektime(xmldir, "#{infotype}-", val)
2505 my_gen_real_thumbnail.call
2507 perform_change_seektime_and_cleanup.call(values[:new])
2509 save_undo(_("specify seektime"),
2511 perform_change_seektime_and_cleanup.call(values[:old])
2512 $notebook.set_page(0)
2514 perform_change_seektime_and_cleanup.call(values[:new])
2515 $notebook.set_page(0)
2521 whitebalance_and_cleanup = proc {
2522 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2523 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2524 perform_change_whitebalance_and_cleanup = proc { |val|
2525 change_whitebalance(xmldir, "#{infotype}-", val)
2526 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2527 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2528 if File.exists?(thumbnail_file)
2529 File.delete(thumbnail_file)
2532 perform_change_whitebalance_and_cleanup.call(values[:new])
2534 save_undo(_("fix white balance"),
2536 perform_change_whitebalance_and_cleanup.call(values[:old])
2537 $notebook.set_page(0)
2539 perform_change_whitebalance_and_cleanup.call(values[:new])
2540 $notebook.set_page(0)
2546 gammacorrect_and_cleanup = proc {
2547 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2548 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2549 perform_change_gammacorrect_and_cleanup = proc { |val|
2550 change_gammacorrect(xmldir, "#{infotype}-", val)
2551 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2552 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2553 if File.exists?(thumbnail_file)
2554 File.delete(thumbnail_file)
2557 perform_change_gammacorrect_and_cleanup.call(values[:new])
2559 save_undo(_("gamma correction"),
2561 perform_change_gammacorrect_and_cleanup.call(values[:old])
2562 $notebook.set_page(0)
2564 perform_change_gammacorrect_and_cleanup.call(values[:new])
2565 $notebook.set_page(0)
2571 enhance_and_cleanup = proc {
2572 perform_enhance_and_cleanup = proc {
2573 enhance(xmldir, "#{infotype}-")
2574 my_gen_real_thumbnail.call
2577 perform_enhance_and_cleanup.call
2579 save_undo(_("enhance"),
2581 perform_enhance_and_cleanup.call
2582 $notebook.set_page(0)
2584 perform_enhance_and_cleanup.call
2585 $notebook.set_page(0)
2590 evtbox.signal_connect('button-press-event') { |w, event|
2591 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2593 rotate_and_cleanup.call(90)
2595 rotate_and_cleanup.call(-90)
2596 elsif $enhance.active?
2597 enhance_and_cleanup.call
2600 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2601 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2602 { :forbid_left => true, :forbid_right => true,
2603 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2604 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2605 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2606 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2607 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2609 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2614 evtbox.signal_connect('button-press-event') { |w, event|
2615 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2619 evtbox.signal_connect('button-release-event') { |w, event|
2620 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2621 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2622 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2623 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2624 msg 3, "gesture rotate: #{angle}"
2625 rotate_and_cleanup.call(angle)
2628 $gesture_press = nil
2631 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2632 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2633 current_y_sub_albums += 1
2636 if $xmldir.child_byname_notattr('dir', 'deleted')
2638 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2639 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2640 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2641 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2642 #- this album image/caption
2643 if $xmldir.attributes['thumbnails-caption']
2644 add_subalbum.call($xmldir, 0)
2647 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2648 $xmldir.elements.each { |element|
2649 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2650 #- element (image or video) of this album
2651 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2652 msg 3, "dest_img: #{dest_img}"
2653 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2654 total[element.name] += 1
2656 if element.name == 'dir' && !element.attributes['deleted']
2657 #- sub-album image/caption
2658 add_subalbum.call(element, subalbums_counter += 1)
2659 total[element.name] += 1
2662 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2663 total['image'], total['video'], total['dir'] ]))
2664 $subalbums_vb.add($subalbums)
2665 $subalbums_vb.show_all
2667 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2668 $notebook.get_tab_label($autotable_sw).sensitive = false
2669 $notebook.set_page(0)
2670 $thumbnails_title.buffer.text = ''
2672 $notebook.get_tab_label($autotable_sw).sensitive = true
2673 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2676 if !$xmldir.child_byname_notattr('dir', 'deleted')
2677 $notebook.get_tab_label($subalbums_sw).sensitive = false
2678 $notebook.set_page(1)
2680 $notebook.get_tab_label($subalbums_sw).sensitive = true
2684 def pixbuf_or_nil(filename)
2686 return Gdk::Pixbuf.new(filename)
2692 def theme_choose(current)
2693 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2695 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2696 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2697 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2699 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2700 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2701 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2702 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2703 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2704 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2705 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2706 treeview.signal_connect('button-press-event') { |w, event|
2707 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2708 dialog.response(Gtk::Dialog::RESPONSE_OK)
2712 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2714 ([ $FPATH + '/themes/simple' ] + (`find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.find_all { |e| e !~ /simple$/ }.sort)).each { |dir|
2717 iter[0] = File.basename(dir)
2718 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2719 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2720 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2721 if File.basename(dir) == current
2722 treeview.selection.select_iter(iter)
2725 dialog.set_default_size(-1, 500)
2726 dialog.vbox.show_all
2728 dialog.run { |response|
2729 iter = treeview.selection.selected
2731 if response == Gtk::Dialog::RESPONSE_OK && iter
2732 return model.get_value(iter, 0)
2738 def show_password_protections
2739 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2740 child_iter = $albums_iters[xmldir.attributes['path']]
2741 if xmldir.attributes['password-protect']
2742 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2743 already_protected = true
2744 elsif already_protected
2745 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2747 pix = pix.saturate_and_pixelate(1, true)
2753 xmldir.elements.each('dir') { |elem|
2754 if !elem.attributes['deleted']
2755 examine_dir_elem.call(child_iter, elem, already_protected)
2759 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2762 def populate_subalbums_treeview(select_first)
2766 $subalbums_vb.children.each { |chld|
2767 $subalbums_vb.remove(chld)
2770 source = $xmldoc.root.attributes['source']
2771 msg 3, "source: #{source}"
2773 xmldir = $xmldoc.elements['//dir']
2774 if !xmldir || xmldir.attributes['path'] != source
2775 msg 1, _("Corrupted booh file...")
2779 append_dir_elem = proc { |parent_iter, xmldir|
2780 child_iter = $albums_ts.append(parent_iter)
2781 child_iter[0] = File.basename(xmldir.attributes['path'])
2782 child_iter[1] = xmldir.attributes['path']
2783 $albums_iters[xmldir.attributes['path']] = child_iter
2784 msg 3, "puttin location: #{xmldir.attributes['path']}"
2785 xmldir.elements.each('dir') { |elem|
2786 if !elem.attributes['deleted']
2787 append_dir_elem.call(child_iter, elem)
2791 append_dir_elem.call(nil, xmldir)
2792 show_password_protections
2794 $albums_tv.expand_all
2796 $albums_tv.selection.select_iter($albums_ts.iter_first)
2800 def select_current_theme
2801 select_theme($xmldoc.root.attributes['theme'],
2802 $xmldoc.root.attributes['limit-sizes'],
2803 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2804 $xmldoc.root.attributes['thumbnails-per-row'])
2807 def open_file(filename)
2811 $current_path = nil #- invalidate
2812 $modified_pixbufs = {}
2815 $subalbums_vb.children.each { |chld|
2816 $subalbums_vb.remove(chld)
2819 if !File.exists?(filename)
2820 return utf8(_("File not found."))
2824 $xmldoc = REXML::Document.new File.new(filename)
2829 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2830 if entry2type(filename).nil?
2831 return utf8(_("Not a booh file!"))
2833 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."))
2837 if !source = $xmldoc.root.attributes['source']
2838 return utf8(_("Corrupted booh file..."))
2841 if !dest = $xmldoc.root.attributes['destination']
2842 return utf8(_("Corrupted booh file..."))
2845 if !theme = $xmldoc.root.attributes['theme']
2846 return utf8(_("Corrupted booh file..."))
2849 if $xmldoc.root.attributes['version'] < '0.9.0'
2850 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2851 mark_document_as_dirty
2852 if $xmldoc.root.attributes['version'] < '0.8.4'
2853 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2854 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2855 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2856 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2857 if old_dest_dir != new_dest_dir
2858 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2860 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2861 xmldir.elements.each { |element|
2862 if %w(image video).include?(element.name) && !element.attributes['deleted']
2863 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2864 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2865 Dir[old_name + '*'].each { |file|
2866 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2867 file != new_file and sys("mv '#{file}' '#{new_file}'")
2870 if element.name == 'dir' && !element.attributes['deleted']
2871 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2872 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2873 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2877 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2881 $xmldoc.root.add_attribute('version', $VERSION)
2884 select_current_theme
2886 $filename = filename
2887 set_mainwindow_title(nil)
2888 $default_size['thumbnails'] =~ /(.*)x(.*)/
2889 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2890 $albums_thumbnail_size =~ /(.*)x(.*)/
2891 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2893 populate_subalbums_treeview(true)
2895 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
2899 def open_file_user(filename)
2900 result = open_file(filename)
2902 $config['last-opens'] ||= []
2903 if $config['last-opens'][-1] != utf8(filename)
2904 $config['last-opens'] << utf8(filename)
2906 $orig_filename = $filename
2907 $main_window.title = 'booh - ' + File.basename($orig_filename)
2908 tmp = Tempfile.new("boohtemp")
2909 Thread.critical = true
2910 $filename = tmp.path
2913 ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
2914 Thread.critical = false
2916 $tempfiles << $filename << "#{$filename}.backup"
2918 $orig_filename = nil
2924 if !ask_save_modifications(utf8(_("Save this album?")),
2925 utf8(_("Do you want to save the changes to this album?")),
2926 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2929 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2931 Gtk::FileChooser::ACTION_OPEN,
2933 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2934 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2935 fc.set_current_folder(File.expand_path("~/.booh"))
2936 fc.transient_for = $main_window
2939 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2940 push_mousecursor_wait(fc)
2941 msg = open_file_user(fc.filename)
2956 def additional_booh_options
2959 options += "--mproc #{$config['mproc'].to_i} "
2961 options += "--comments-format '#{$config['comments-format']}' "
2962 if $config['transcode-videos']
2963 options += "--transcode-videos '#{$config['transcode-videos']}' "
2968 def ask_multi_languages(value)
2970 spl = value.split(',')
2971 value = [ spl[0..-2], spl[-1] ]
2974 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
2977 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2978 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2980 lbl = Gtk::Label.new
2982 _("You can choose to activate <b>multi-languages</b> support for this web-album
2983 (it will work only if you publish your web-album on an Apache web-server). This will
2984 use the MultiViews feature of Apache; the pages will be served according to the
2985 value of the Accept-Language HTTP header sent by the web browsers, so that people
2986 with different languages preferences will be able to browse your web-album with
2987 navigation in their language (if language is available).
2990 dialog.vbox.add(lbl)
2991 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
2992 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
2993 add(languages = Gtk::Button.new))))
2995 pick_languages = proc {
2996 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
2999 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3000 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3002 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3003 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3005 SUPPORTED_LANGUAGES.each { |lang|
3006 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3007 if ! value.nil? && value[0].include?(lang)
3013 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3014 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3015 fallback_language = nil
3016 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3017 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3018 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3019 fbl_rb.active = true
3020 fallback_language = SUPPORTED_LANGUAGES[0]
3022 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3023 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3024 rb.signal_connect('clicked') { fallback_language = lang }
3025 if ! value.nil? && value[1] == lang
3030 dialog2.window_position = Gtk::Window::POS_MOUSE
3034 dialog2.run { |response|
3036 if resp == Gtk::Dialog::RESPONSE_OK
3038 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3039 value[1] = fallback_language
3040 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3047 languages.signal_connect('clicked') {
3050 dialog.window_position = Gtk::Window::POS_MOUSE
3054 rb_yes.active = true
3055 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3057 rb_no.signal_connect('clicked') {
3061 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3074 dialog.run { |response|
3079 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3081 return [ true, nil ]
3083 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3092 if !ask_save_modifications(utf8(_("Save this album?")),
3093 utf8(_("Do you want to save the changes to this album?")),
3094 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3097 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3099 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3100 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3101 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3103 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3104 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3105 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3106 tbl.attach(src = Gtk::Entry.new,
3107 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3108 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3109 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3110 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3111 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3112 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3113 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3114 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3115 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3116 tbl.attach(dest = Gtk::Entry.new,
3117 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3118 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3119 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3120 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3121 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3122 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3123 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3124 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3125 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3127 tooltips = Gtk::Tooltips.new
3128 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3129 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3130 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3131 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3132 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3133 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3134 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)
3135 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3136 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3137 nperpage_model = Gtk::ListStore.new(String, String)
3138 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3139 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3140 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3141 nperpagecombo.set_attributes(crt, { :markup => 0 })
3142 iter = nperpage_model.append
3143 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3145 [ 12, 20, 30, 40, 50 ].each { |v|
3146 iter = nperpage_model.append
3147 iter[0] = iter[1] = v.to_s
3149 nperpagecombo.active = 0
3151 multilanguages_value = nil
3152 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3153 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3154 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)
3155 multilanguages.signal_connect('clicked') {
3156 retval = ask_multi_languages(multilanguages_value)
3158 multilanguages_value = retval[1]
3160 if multilanguages_value
3161 ml_label.text = utf8(_("Multi-languages: enabled."))
3163 ml_label.text = utf8(_("Multi-languages: disabled."))
3166 if $config['default-multi-languages']
3167 multilanguages_value = $config['default-multi-languages']
3168 ml_label.text = utf8(_("Multi-languages: enabled."))
3170 ml_label.text = utf8(_("Multi-languages: disabled."))
3173 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3174 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3175 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)
3176 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3177 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3178 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)
3180 src_nb_calculated_for = ''
3182 process_src_nb = proc {
3183 if src.text != src_nb_calculated_for
3184 src_nb_calculated_for = src.text
3186 Thread.kill(src_nb_thread)
3189 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3190 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3192 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3193 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3194 src_nb_thread = Thread.new {
3195 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3196 total = { 'image' => 0, 'video' => 0, nil => 0 }
3197 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3198 if File.basename(dir) =~ /^\./
3202 Dir.entries(dir.chomp).each { |file|
3203 total[entry2type(file)] += 1
3205 rescue Errno::EACCES, Errno::ENOENT
3209 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3213 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3216 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3222 timeout_src_nb = Gtk.timeout_add(100) {
3226 src_browse.signal_connect('clicked') {
3227 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3229 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3231 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3232 fc.transient_for = $main_window
3233 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3234 src.text = utf8(fc.filename)
3236 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3241 dest_browse.signal_connect('clicked') {
3242 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3244 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3246 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3247 fc.transient_for = $main_window
3248 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3249 dest.text = utf8(fc.filename)
3254 conf_browse.signal_connect('clicked') {
3255 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3257 Gtk::FileChooser::ACTION_SAVE,
3259 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3260 fc.transient_for = $main_window
3261 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3262 fc.set_current_folder(File.expand_path("~/.booh"))
3263 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3264 conf.text = utf8(fc.filename)
3271 recreate_theme_config = proc {
3272 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3274 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3275 $images_size.each { |s|
3276 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3280 tooltips.set_tip(cb, utf8(s['description']), nil)
3281 theme_sizes << { :widget => cb, :value => s['name'] }
3283 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3284 tooltips = Gtk::Tooltips.new
3285 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3286 theme_sizes << { :widget => cb, :value => 'original' }
3289 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3292 $allowed_N_values.each { |n|
3294 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3296 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3298 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3302 nperrows << { :widget => rb, :value => n }
3304 nperrowradios.show_all
3306 recreate_theme_config.call
3308 theme_button.signal_connect('clicked') {
3309 if newtheme = theme_choose(theme_button.label)
3310 theme_button.label = newtheme
3311 recreate_theme_config.call
3315 dialog.vbox.add(frame1)
3316 dialog.vbox.add(frame2)
3322 dialog.run { |response|
3323 if response == Gtk::Dialog::RESPONSE_OK
3324 srcdir = from_utf8_safe(src.text)
3325 destdir = from_utf8_safe(dest.text)
3326 confpath = from_utf8_safe(conf.text)
3327 if src.text != '' && srcdir == ''
3328 show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3330 elsif !File.directory?(srcdir)
3331 show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3333 elsif dest.text != '' && destdir == ''
3334 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3336 elsif destdir != make_dest_filename(destdir)
3337 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3339 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3340 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3341 inside it will be permanently removed before creating the web-album!
3342 Are you sure you want to continue?")), { :okcancel => true })
3344 elsif File.exists?(destdir) && !File.directory?(destdir)
3345 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3347 elsif conf.text == ''
3348 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3350 elsif conf.text != '' && confpath == ''
3351 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3353 elsif File.directory?(confpath)
3354 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3356 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3357 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3359 system("mkdir '#{destdir}'")
3360 if !File.directory?(destdir)
3361 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3373 srcdir = from_utf8(src.text)
3374 destdir = from_utf8(dest.text)
3375 configskel = File.expand_path(from_utf8(conf.text))
3376 theme = theme_button.label
3377 #- some sort of automatic theme preference
3378 $config['default-theme'] = theme
3379 $config['default-multi-languages'] = multilanguages_value
3380 $config['default-optimize32'] = optimize432.active?.to_s
3381 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3382 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3383 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3384 opt432 = optimize432.active?
3385 madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3386 indexlink = indexlinkentry.text.gsub('\'', ''')
3389 Thread.kill(src_nb_thread)
3390 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3393 Gtk.timeout_remove(timeout_src_nb)
3396 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3397 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3398 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3399 (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
3400 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3401 utf8(_("Please wait while scanning source directory...")),
3403 { :closure_after => proc {
3404 open_file_user(configskel)
3405 $main_window.urgency_hint = true
3411 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3413 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3414 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3415 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3417 source = $xmldoc.root.attributes['source']
3418 dest = $xmldoc.root.attributes['destination']
3419 theme = $xmldoc.root.attributes['theme']
3420 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3421 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3422 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3423 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3425 limit_sizes = limit_sizes.split(/,/)
3427 madewith = ($xmldoc.root.attributes['made-with'] || '').gsub(''', '\'')
3428 indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub(''', '\'')
3429 save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3431 tooltips = Gtk::Tooltips.new
3432 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3433 tbl.attach(Gtk::Label.new(utf8(_("Directory of source photos/videos: "))),
3434 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3435 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3436 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3437 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3438 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3439 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3440 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3441 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3442 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3443 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3444 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3446 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3447 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3448 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3449 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3450 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3451 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3452 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)
3453 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3454 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3455 nperpage_model = Gtk::ListStore.new(String, String)
3456 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3457 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3458 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3459 nperpagecombo.set_attributes(crt, { :markup => 0 })
3460 iter = nperpage_model.append
3461 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3463 [ 12, 20, 30, 40, 50 ].each { |v|
3464 iter = nperpage_model.append
3465 iter[0] = iter[1] = v.to_s
3466 if nperpage && nperpage == v.to_s
3467 nperpagecombo.active_iter = iter
3470 if nperpagecombo.active_iter.nil?
3471 nperpagecombo.active = 0
3474 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3475 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3476 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)
3478 if save_multilanguages_value
3479 ml_label.text = utf8(_("Multi-languages: enabled."))
3481 ml_label.text = utf8(_("Multi-languages: disabled."))
3485 multilanguages.signal_connect('clicked') {
3486 retval = ask_multi_languages(save_multilanguages_value)
3488 save_multilanguages_value = retval[1]
3493 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3494 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3496 indexlinkentry.text = indexlink
3498 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)
3499 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3500 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3502 madewithentry.text = madewith
3504 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)
3508 recreate_theme_config = proc {
3509 theme_sizes.each { |e| sizes.remove(e[:widget]) }