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-2006 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/gtkadds'
28 require 'booh/GtkAutoTable'
32 bindtextdomain("booh")
34 require 'rexml/document'
37 require 'booh/booh-lib'
39 require 'booh/UndoHandler'
44 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
46 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
49 #- default values for some globals
53 $ignore_videos = false
54 $button1_pressed_autotable = false
55 $generated_outofline = false
58 puts _("Usage: %s [OPTION]...") % File.basename($0)
60 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
65 parser = GetoptLong.new
66 parser.set_options(*$options.collect { |ary| ary[0..2] })
68 parser.each_option do |name, arg|
74 when '--verbose-level'
75 $verbose_level = arg.to_i
88 $config_file = File.expand_path('~/.booh-gui-rc')
89 if File.readable?($config_file)
90 $xmldoc = REXML::Document.new(File.new($config_file))
91 $xmldoc.root.elements.each { |element|
92 txt = element.get_text
94 if txt.value =~ /~~~/ || element.name == 'last-opens'
95 $config[element.name] = txt.value.split(/~~~/)
97 $config[element.name] = txt.value
99 elsif element.elements.size == 0
100 $config[element.name] = ''
102 $config[element.name] = {}
103 element.each { |chld|
105 $config[element.name][chld.name] = txt ? txt.value : nil
110 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
111 $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
112 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
113 $config['comments-format'] ||= '%t'
114 if !FileTest.directory?(File.expand_path('~/.booh'))
115 system("mkdir ~/.booh")
123 if !system("which convert >/dev/null 2>/dev/null")
124 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
125 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
128 if !system("which identify >/dev/null 2>/dev/null")
129 show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
130 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
132 missing = %w(transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
134 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
137 viewer_binary = $config['video-viewer'].split.first
138 if viewer_binary && !File.executable?(viewer_binary)
139 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
140 You should fix this in Edit/Preferences so that you can view videos.
142 Problem was: '%s' is not an executable file.
143 Hint: don't forget to specify the full path to the executable,
144 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
146 image_editor_binary = $config['image-editor'].split.first
147 if image_editor_binary && !File.executable?(image_editor_binary)
148 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
149 You should fix this in Edit/Preferences so that you can edit images externally.
151 Problem was: '%s' is not an executable file.
152 Hint: don't forget to specify the full path to the executable,
153 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
155 browser_binary = $config['browser'].split.first
156 if browser_binary && !File.executable?(browser_binary)
157 show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
158 You should fix this in Edit/Preferences so that you can open URLs.
160 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
165 if $config['last-opens'] && $config['last-opens'].size > 10
166 $config['last-opens'] = $config['last-opens'][-10, 10]
169 ios = File.open($config_file, "w")
170 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
171 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
172 $config.each_pair { |key, value|
173 elem = $xmldoc.root.add_element key
175 $config[key].each_pair { |subkey, subvalue|
176 subelem = elem.add_element subkey
177 subelem.add_text subvalue.to_s
179 elsif value.is_a? Array
180 elem.add_text value.join('~~~')
185 elem.add_text value.to_s
189 $xmldoc.write(ios, 0)
192 $tempfiles.each { |f|
197 def set_mousecursor(what, *widget)
198 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
199 if widget[0] && widget[0].window
200 widget[0].window.cursor = cursor
202 if $main_window && $main_window.window
203 $main_window.window.cursor = cursor
205 $current_cursor = what
207 def set_mousecursor_wait(*widget)
208 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
209 if Thread.current == Thread.main
210 Gtk.main_iteration while Gtk.events_pending?
213 def set_mousecursor_normal(*widget)
214 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
216 def push_mousecursor_wait(*widget)
217 if $current_cursor != Gdk::Cursor::WATCH
218 $save_cursor = $current_cursor
219 gtk_thread_protect { set_mousecursor_wait(*widget) }
222 def pop_mousecursor(*widget)
223 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
227 source = $xmldoc.root.attributes['source']
228 dest = $xmldoc.root.attributes['destination']
229 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
232 def full_src_dir_to_rel(path, source)
233 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
236 def build_full_dest_filename(filename)
237 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
240 def save_undo(name, closure, *params)
241 UndoHandler.save_undo(name, closure, [ *params ])
242 $undo_tb.sensitive = $undo_mb.sensitive = true
243 $redo_tb.sensitive = $redo_mb.sensitive = false
246 def view_element(filename, closures)
247 if entry2type(filename) == 'video'
248 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
254 w = Gtk::Window.new.set_title(filename)
256 msg 3, "filename: #{filename}"
257 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
258 #- typically this file won't exist in case of videos; try with the largest thumbnail around
259 if !File.exists?(dest_img)
260 if entry2type(filename) == 'video'
261 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
262 if not alternatives.empty?
263 dest_img = alternatives[-1]
266 push_mousecursor_wait
267 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
269 if !File.exists?(dest_img)
270 msg 2, _("Could not generate fullscreen thumbnail!")
275 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)))
276 evt.signal_connect('button-press-event') { |this, event|
277 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
278 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
280 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
282 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
283 delete_item.signal_connect('activate') {
285 closures[:delete].call
288 menu.popup(nil, nil, event.button, event.time)
291 evt.signal_connect('button-release-event') { |this, event|
293 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
294 msg 3, "gesture delete: click-drag right button to the bottom"
296 closures[:delete].call
297 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
301 tooltips = Gtk::Tooltips.new
302 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
304 w.signal_connect('key-press-event') { |w,event|
305 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
307 closures[:delete].call
311 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
312 b.signal_connect('clicked') { w.destroy }
315 vb.pack_start(evt, false, false)
316 vb.pack_end(bottom, false, false)
319 w.signal_connect('delete-event') { w.destroy }
320 w.window_position = Gtk::Window::POS_CENTER
324 def scroll_upper(scrolledwindow, ypos_top)
325 newval = scrolledwindow.vadjustment.value -
326 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
327 if newval < scrolledwindow.vadjustment.lower
328 newval = scrolledwindow.vadjustment.lower
330 scrolledwindow.vadjustment.value = newval
333 def scroll_lower(scrolledwindow, ypos_bottom)
334 newval = scrolledwindow.vadjustment.value +
335 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
336 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
337 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
339 scrolledwindow.vadjustment.value = newval
342 def autoscroll_if_needed(scrolledwindow, image, textview)
343 #- autoscroll if cursor or image is not visible, if possible
344 if image && image.window || textview.window
345 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
346 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
347 current_miny_visible = scrolledwindow.vadjustment.value
348 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
349 if ypos_top < current_miny_visible
350 scroll_upper(scrolledwindow, ypos_top)
351 elsif ypos_bottom > current_maxy_visible
352 scroll_lower(scrolledwindow, ypos_bottom)
357 def create_editzone(scrolledwindow, pagenum, image)
358 frame = Gtk::Frame.new
359 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
360 frame.set_shadow_type(Gtk::SHADOW_IN)
361 textview.signal_connect('key-press-event') { |w, event|
362 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
363 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
364 scrolledwindow.signal_emit('key-press-event', event)
366 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
367 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
368 if event.keyval == Gdk::Keyval::GDK_Up
369 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
370 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
372 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
375 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
376 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
378 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
385 candidate_undo_text = nil
386 textview.signal_connect('focus-in-event') { |w, event|
387 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
388 candidate_undo_text = textview.buffer.text
392 textview.signal_connect('key-release-event') { |w, event|
393 if candidate_undo_text && candidate_undo_text != textview.buffer.text
395 save_undo(_("text edit"),
397 save_text = textview.buffer.text
398 textview.buffer.text = text
400 $notebook.set_page(pagenum)
402 textview.buffer.text = save_text
404 $notebook.set_page(pagenum)
406 }, candidate_undo_text)
407 candidate_undo_text = nil
410 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)
411 autoscroll_if_needed(scrolledwindow, image, textview)
416 return [ frame, textview ]
419 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
421 if !$modified_pixbufs[thumbnail_img]
422 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
423 elsif !$modified_pixbufs[thumbnail_img][:orig]
424 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
427 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
430 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
431 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
432 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
433 if pixbuf.height > desired_y
434 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
435 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
436 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
441 if $modified_pixbufs[thumbnail_img][:whitebalance]
442 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
445 #- fix gamma correction
446 if $modified_pixbufs[thumbnail_img][:gammacorrect]
447 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
450 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
453 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
456 #- update rotate attribute
457 xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
459 $modified_pixbufs[thumbnail_img] ||= {}
460 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
461 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
463 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
466 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
469 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
471 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
473 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
474 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
476 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
477 $notebook.set_page(0)
478 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
483 def color_swap(xmldir, attributes_prefix)
485 if xmldir.attributes["#{attributes_prefix}color-swap"]
486 xmldir.delete_attribute("#{attributes_prefix}color-swap")
488 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
492 def enhance(xmldir, attributes_prefix)
494 if xmldir.attributes["#{attributes_prefix}enhance"]
495 xmldir.delete_attribute("#{attributes_prefix}enhance")
497 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
501 def change_frame_offset(xmldir, attributes_prefix, value)
503 xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
506 def ask_new_frame_offset(xmldir, attributes_prefix)
508 value = xmldir.attributes["#{attributes_prefix}frame-offset"]
513 dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
515 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
516 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
517 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
521 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
522 from. There are approximately 25 frames per second in a video.
525 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
526 entry.signal_connect('key-press-event') { |w, event|
527 if event.keyval == Gdk::Keyval::GDK_Return
528 dialog.response(Gtk::Dialog::RESPONSE_OK)
530 elsif event.keyval == Gdk::Keyval::GDK_Escape
531 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
534 false #- propagate if needed
538 dialog.window_position = Gtk::Window::POS_MOUSE
541 dialog.run { |response|
544 if response == Gtk::Dialog::RESPONSE_OK
546 msg 3, "changing frame offset to #{newval}"
547 return { :old => value, :new => newval }
554 def change_pano_amount(xmldir, attributes_prefix, value)
557 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
559 xmldir.add_attribute("#{attributes_prefix}pano-amount", value)
563 def ask_new_pano_amount(xmldir, attributes_prefix)
565 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
570 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
572 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
573 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
574 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
578 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
579 of this panorama image compared to other regular images. For example, if the panorama
580 was taken out of four photos on one row, counting the necessary overlap, the width of
581 this panorama image should probably be roughly three times the width of regular images.
583 With this information, booh will be able to generate panorama thumbnails looking
587 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)")))).
588 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
589 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
590 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
591 spin.signal_connect('value-changed') {
594 dialog.window_position = Gtk::Window::POS_MOUSE
597 spin.value = value.to_f
604 dialog.run { |response|
608 newval = spin.value.to_f
611 if response == Gtk::Dialog::RESPONSE_OK
613 msg 3, "changing panorama amount to #{newval}"
614 return { :old => value, :new => newval }
621 def change_whitebalance(xmlelem, attributes_prefix, value)
623 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
626 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
628 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
629 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
630 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
631 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
632 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
633 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
634 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
635 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
636 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
637 $modified_pixbufs[thumbnail_img] ||= {}
638 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
639 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
641 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
642 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
644 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
647 $modified_pixbufs[thumbnail_img] ||= {}
648 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
650 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
653 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
654 #- init $modified_pixbufs correctly
655 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
657 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
659 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
661 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
662 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
663 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
667 _("You can fix the <b>white balance</b> of the image, if your image is too blue
668 or too yellow because your camera didn't detect the light correctly. Drag the
669 slider below the image to the left for more blue, to the right for more yellow.
673 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
675 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
677 dialog.window_position = Gtk::Window::POS_MOUSE
681 timeout = Gtk.timeout_add(100) {
682 if hs.value != lastval
685 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
691 dialog.run { |response|
692 Gtk.timeout_remove(timeout)
693 if response == Gtk::Dialog::RESPONSE_OK
695 newval = hs.value.to_s
696 msg 3, "changing white balance to #{newval}"
698 return { :old => value, :new => newval }
701 $modified_pixbufs[thumbnail_img] ||= {}
702 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
703 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
711 def change_gammacorrect(xmlelem, attributes_prefix, value)
713 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
716 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
718 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
719 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
720 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
721 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
722 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
723 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
724 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
725 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
726 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
727 $modified_pixbufs[thumbnail_img] ||= {}
728 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
729 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
731 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
732 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
734 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
737 $modified_pixbufs[thumbnail_img] ||= {}
738 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
740 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
743 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
744 #- init $modified_pixbufs correctly
745 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
747 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
749 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
751 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
752 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
753 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
757 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
758 or too bright. Drag the slider below the image.
762 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
764 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
766 dialog.window_position = Gtk::Window::POS_MOUSE
770 timeout = Gtk.timeout_add(100) {
771 if hs.value != lastval
774 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
780 dialog.run { |response|
781 Gtk.timeout_remove(timeout)
782 if response == Gtk::Dialog::RESPONSE_OK
784 newval = hs.value.to_s
785 msg 3, "gamma correction to #{newval}"
787 return { :old => value, :new => newval }
790 $modified_pixbufs[thumbnail_img] ||= {}
791 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
792 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
800 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
801 system("rm -f '#{destfile}'")
802 #- type can be 'element' or 'subdir'
804 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
806 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
810 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
812 push_mousecursor_wait
813 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
816 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
822 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
823 distribute_multiple_call = Proc.new { |action, arg|
824 $selected_elements.each_key { |path|
825 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
827 if possible_actions[:can_multiple] && $selected_elements.length > 0
828 UndoHandler.begin_batch
829 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
830 UndoHandler.end_batch
832 closures[action].call(arg)
834 $selected_elements = {}
837 if optionals.include?('change_image')
838 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
839 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
840 changeimg.signal_connect('activate') { closures[:change].call }
841 menu.append(Gtk::SeparatorMenuItem.new)
843 if !possible_actions[:can_multiple] || $selected_elements.length == 0
846 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
847 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
848 view.signal_connect('activate') { closures[:view].call }
850 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
851 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
852 view.signal_connect('activate') { closures[:view].call }
853 menu.append(Gtk::SeparatorMenuItem.new)
856 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
857 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
858 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
859 exif.signal_connect('activate') { show_popup($main_window,
860 utf8(`identify -format "%[EXIF:*]" #{fullpath}`.sub(/MakerNote.*\n/, '')),
861 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
862 menu.append(Gtk::SeparatorMenuItem.new)
865 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
866 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
867 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
868 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
869 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
870 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
871 if !possible_actions[:can_multiple] || $selected_elements.length == 0
872 menu.append(Gtk::SeparatorMenuItem.new)
873 if !possible_actions[:forbid_left]
874 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
875 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
876 moveleft.signal_connect('activate') { closures[:move].call('left') }
877 if !possible_actions[:can_left]
878 moveleft.sensitive = false
881 if !possible_actions[:forbid_right]
882 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
883 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
884 moveright.signal_connect('activate') { closures[:move].call('right') }
885 if !possible_actions[:can_right]
886 moveright.sensitive = false
889 if optionals.include?('move_top')
890 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
891 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
892 movetop.signal_connect('activate') { closures[:move].call('top') }
893 if !possible_actions[:can_top]
894 movetop.sensitive = false
897 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
898 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
899 moveup.signal_connect('activate') { closures[:move].call('up') }
900 if !possible_actions[:can_up]
901 moveup.sensitive = false
903 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
904 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
905 movedown.signal_connect('activate') { closures[:move].call('down') }
906 if !possible_actions[:can_down]
907 movedown.sensitive = false
909 if optionals.include?('move_bottom')
910 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
911 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
912 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
913 if !possible_actions[:can_bottom]
914 movebottom.sensitive = false
919 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
920 menu.append(Gtk::SeparatorMenuItem.new)
921 menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
922 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
923 color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
924 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
925 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
926 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
927 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
928 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
929 frame_offset.signal_connect('activate') {
930 if possible_actions[:can_multiple] && $selected_elements.length > 0
931 if values = ask_new_frame_offset(nil, '')
932 distribute_multiple_call.call(:frame_offset, values)
935 closures[:frame_offset].call
940 menu.append( Gtk::SeparatorMenuItem.new)
941 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
942 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
943 gammacorrect.signal_connect('activate') {
944 if possible_actions[:can_multiple] && $selected_elements.length > 0
945 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
946 distribute_multiple_call.call(:gammacorrect, values)
949 closures[:gammacorrect].call
952 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
953 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
954 whitebalance.signal_connect('activate') {
955 if possible_actions[:can_multiple] && $selected_elements.length > 0
956 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
957 distribute_multiple_call.call(:whitebalance, values)
960 closures[:whitebalance].call
963 if !possible_actions[:can_multiple] || $selected_elements.length == 0
964 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
965 _("Enhance constrast"))))
967 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
969 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
970 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
971 if type == 'image' && possible_actions[:can_panorama]
972 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
973 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
974 panorama.signal_connect('activate') {
975 if possible_actions[:can_multiple] && $selected_elements.length > 0
976 if values = ask_new_pano_amount(nil, '')
977 distribute_multiple_call.call(:pano, values)
980 distribute_multiple_call.call(:pano)
984 menu.append( Gtk::SeparatorMenuItem.new)
985 if optionals.include?('delete')
986 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
987 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
988 if !possible_actions[:can_multiple] || $selected_elements.length == 0
989 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
990 paste_item.signal_connect('activate') { closures[:paste].call }
991 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
992 clear_item.signal_connect('activate') { $cuts = [] }
994 paste_item.sensitive = clear_item.sensitive = false
997 menu.append( Gtk::SeparatorMenuItem.new)
999 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1000 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1001 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1002 editexternally.signal_connect('activate') {
1003 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1008 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1009 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1010 if optionals.include?('delete')
1011 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1012 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1015 menu.popup(nil, nil, event.button, event.time)
1018 def delete_current_subalbum
1020 sel = $albums_tv.selection.selected_rows
1021 $xmldir.elements.each { |e|
1022 if e.name == 'image' || e.name == 'video'
1023 e.add_attribute('deleted', 'true')
1026 #- branch if we have a non deleted subalbum
1027 if $xmldir.child_byname_notattr('dir', 'deleted')
1028 $xmldir.delete_attribute('thumbnails-caption')
1029 $xmldir.delete_attribute('thumbnails-captionfile')
1031 $xmldir.add_attribute('deleted', 'true')
1033 while moveup.parent.name == 'dir'
1034 moveup = moveup.parent
1035 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1036 moveup.add_attribute('deleted', 'true')
1043 save_changes('forced')
1044 populate_subalbums_treeview(false)
1045 $albums_tv.selection.select_path(sel[0])
1051 $current_path = nil #- prevent save_changes from being rerun again
1052 sel = $albums_tv.selection.selected_rows
1053 restore_one = proc { |xmldir|
1054 xmldir.elements.each { |e|
1055 if e.name == 'dir' && e.attributes['deleted']
1058 e.delete_attribute('deleted')
1061 restore_one.call($xmldir)
1062 populate_subalbums_treeview(false)
1063 $albums_tv.selection.select_path(sel[0])
1066 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1069 frame1 = Gtk::Frame.new
1070 fullpath = from_utf8("#{$current_path}/#{filename}")
1072 my_gen_real_thumbnail = proc {
1073 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1077 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1078 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1079 pack_start(img = Gtk::Image.new).
1080 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1081 px, mask = pxb.render_pixmap_and_mask
1082 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1083 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1085 frame1.add(img = Gtk::Image.new)
1088 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1089 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1090 my_gen_real_thumbnail.call
1092 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1095 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1097 tooltips = Gtk::Tooltips.new
1098 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1099 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1101 frame2, textview = create_editzone($autotable_sw, 1, img)
1102 textview.buffer.text = caption
1103 textview.set_justification(Gtk::Justification::CENTER)
1105 vbox = Gtk::VBox.new(false, 5)
1106 vbox.pack_start(evtbox, false, false)
1107 vbox.pack_start(frame2, false, false)
1108 autotable.append(vbox, filename)
1110 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1111 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1113 #- to be able to find widgets by name
1114 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1116 cleanup_all_thumbnails = proc {
1117 #- remove out of sync images
1118 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1119 for sizeobj in $images_size
1120 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
1126 cleanup_all_thumbnails.call
1127 my_gen_real_thumbnail.call
1130 rotate_and_cleanup = proc { |angle|
1131 cleanup_all_thumbnails.call
1132 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1135 move = proc { |direction|
1136 do_method = "move_#{direction}"
1137 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1139 done = autotable.method(do_method).call(vbox)
1140 textview.grab_focus #- because if moving, focus is stolen
1144 save_undo(_("move %s") % direction,
1146 autotable.method(undo_method).call(vbox)
1147 textview.grab_focus #- because if moving, focus is stolen
1148 autoscroll_if_needed($autotable_sw, img, textview)
1149 $notebook.set_page(1)
1151 autotable.method(do_method).call(vbox)
1152 textview.grab_focus #- because if moving, focus is stolen
1153 autoscroll_if_needed($autotable_sw, img, textview)
1154 $notebook.set_page(1)
1160 color_swap_and_cleanup = proc {
1161 perform_color_swap_and_cleanup = proc {
1162 cleanup_all_thumbnails.call
1163 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1164 my_gen_real_thumbnail.call
1167 perform_color_swap_and_cleanup.call
1169 save_undo(_("color swap"),
1171 perform_color_swap_and_cleanup.call
1173 autoscroll_if_needed($autotable_sw, img, textview)
1174 $notebook.set_page(1)
1176 perform_color_swap_and_cleanup.call
1178 autoscroll_if_needed($autotable_sw, img, textview)
1179 $notebook.set_page(1)
1184 change_frame_offset_and_cleanup_real = proc { |values|
1185 perform_change_frame_offset_and_cleanup = proc { |val|
1186 cleanup_all_thumbnails.call
1187 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
1188 my_gen_real_thumbnail.call
1190 perform_change_frame_offset_and_cleanup.call(values[:new])
1192 save_undo(_("specify frame offset"),
1194 perform_change_frame_offset_and_cleanup.call(values[:old])
1196 autoscroll_if_needed($autotable_sw, img, textview)
1197 $notebook.set_page(1)
1199 perform_change_frame_offset_and_cleanup.call(values[:new])
1201 autoscroll_if_needed($autotable_sw, img, textview)
1202 $notebook.set_page(1)
1207 change_frame_offset_and_cleanup = proc {
1208 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
1209 change_frame_offset_and_cleanup_real.call(values)
1213 change_pano_amount_and_cleanup_real = proc { |values|
1214 perform_change_pano_amount_and_cleanup = proc { |val|
1215 cleanup_all_thumbnails.call
1216 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1218 perform_change_pano_amount_and_cleanup.call(values[:new])
1220 save_undo(_("change panorama amount"),
1222 perform_change_pano_amount_and_cleanup.call(values[:old])
1224 autoscroll_if_needed($autotable_sw, img, textview)
1225 $notebook.set_page(1)
1227 perform_change_pano_amount_and_cleanup.call(values[:new])
1229 autoscroll_if_needed($autotable_sw, img, textview)
1230 $notebook.set_page(1)
1235 change_pano_amount_and_cleanup = proc {
1236 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1237 change_pano_amount_and_cleanup_real.call(values)
1241 whitebalance_and_cleanup_real = proc { |values|
1242 perform_change_whitebalance_and_cleanup = proc { |val|
1243 cleanup_all_thumbnails.call
1244 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1245 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1246 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1248 perform_change_whitebalance_and_cleanup.call(values[:new])
1250 save_undo(_("fix white balance"),
1252 perform_change_whitebalance_and_cleanup.call(values[:old])
1254 autoscroll_if_needed($autotable_sw, img, textview)
1255 $notebook.set_page(1)
1257 perform_change_whitebalance_and_cleanup.call(values[:new])
1259 autoscroll_if_needed($autotable_sw, img, textview)
1260 $notebook.set_page(1)
1265 whitebalance_and_cleanup = proc {
1266 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1267 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1268 whitebalance_and_cleanup_real.call(values)
1272 gammacorrect_and_cleanup_real = proc { |values|
1273 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1274 cleanup_all_thumbnails.call
1275 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1276 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1277 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1279 perform_change_gammacorrect_and_cleanup.call(values[:new])
1281 save_undo(_("gamma correction"),
1283 perform_change_gammacorrect_and_cleanup.call(values[:old])
1285 autoscroll_if_needed($autotable_sw, img, textview)
1286 $notebook.set_page(1)
1288 perform_change_gammacorrect_and_cleanup.call(values[:new])
1290 autoscroll_if_needed($autotable_sw, img, textview)
1291 $notebook.set_page(1)
1296 gammacorrect_and_cleanup = Proc.new {
1297 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1298 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1299 gammacorrect_and_cleanup_real.call(values)
1303 enhance_and_cleanup = proc {
1304 perform_enhance_and_cleanup = proc {
1305 cleanup_all_thumbnails.call
1306 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1307 my_gen_real_thumbnail.call
1310 cleanup_all_thumbnails.call
1311 perform_enhance_and_cleanup.call
1313 save_undo(_("enhance"),
1315 perform_enhance_and_cleanup.call
1317 autoscroll_if_needed($autotable_sw, img, textview)
1318 $notebook.set_page(1)
1320 perform_enhance_and_cleanup.call
1322 autoscroll_if_needed($autotable_sw, img, textview)
1323 $notebook.set_page(1)
1328 delete = proc { |isacut|
1329 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 })
1332 perform_delete = proc {
1333 after = autotable.get_next_widget(vbox)
1335 after = autotable.get_previous_widget(vbox)
1337 if $config['deleteondisk'] && !isacut
1338 msg 3, "scheduling for delete: #{fullpath}"
1339 $todelete << fullpath
1341 autotable.remove(vbox)
1343 $vbox2widgets[after][:textview].grab_focus
1344 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1348 previous_pos = autotable.get_current_number(vbox)
1352 delete_current_subalbum
1354 save_undo(_("delete"),
1356 autotable.reinsert(pos, vbox, filename)
1357 $notebook.set_page(1)
1358 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1360 msg 3, "removing deletion schedule of: #{fullpath}"
1361 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1364 $notebook.set_page(1)
1373 $cuts << { :vbox => vbox, :filename => filename }
1374 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1379 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1382 autotable.queue_draws << proc {
1383 $vbox2widgets[last[:vbox]][:textview].grab_focus
1384 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1386 save_undo(_("paste"),
1388 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1389 $notebook.set_page(1)
1392 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1394 $notebook.set_page(1)
1397 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1402 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1403 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1404 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1405 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1407 textview.signal_connect('key-press-event') { |w, event|
1410 x, y = autotable.get_current_pos(vbox)
1411 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1412 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1413 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1414 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1416 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1417 $vbox2widgets[widget_up][:textview].grab_focus
1424 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1426 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1427 $vbox2widgets[widget_down][:textview].grab_focus
1434 if event.keyval == Gdk::Keyval::GDK_Left
1437 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1444 rotate_and_cleanup.call(-90)
1447 if event.keyval == Gdk::Keyval::GDK_Right
1448 next_ = autotable.get_next_widget(vbox)
1449 if next_ && autotable.get_current_pos(next_)[0] > x
1451 $vbox2widgets[next_][:textview].grab_focus
1458 rotate_and_cleanup.call(90)
1461 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1464 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1465 view_element(filename, { :delete => delete })
1468 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1471 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1475 !propagate #- propagate if needed
1478 $ignore_next_release = false
1479 evtbox.signal_connect('button-press-event') { |w, event|
1480 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1481 if event.state & Gdk::Window::BUTTON3_MASK != 0
1482 #- gesture redo: hold right mouse button then click left mouse button
1483 $config['nogestures'] or perform_redo
1484 $ignore_next_release = true
1486 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1488 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1490 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1491 elsif $enhance.active?
1492 enhance_and_cleanup.call
1493 elsif $delete.active?
1497 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1500 $button1_pressed_autotable = true
1501 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1502 if event.state & Gdk::Window::BUTTON1_MASK != 0
1503 #- gesture undo: hold left mouse button then click right mouse button
1504 $config['nogestures'] or perform_undo
1505 $ignore_next_release = true
1507 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1508 view_element(filename, { :delete => delete })
1513 evtbox.signal_connect('button-release-event') { |w, event|
1514 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1515 if !$ignore_next_release
1516 x, y = autotable.get_current_pos(vbox)
1517 next_ = autotable.get_next_widget(vbox)
1518 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1519 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1520 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1521 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1522 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1523 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1524 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1526 $ignore_next_release = false
1527 $gesture_press = nil
1532 #- handle reordering with drag and drop
1533 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1534 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1535 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1536 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1539 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1541 #- mouse gesture first (dnd disables button-release-event)
1542 if $gesture_press && $gesture_press[:filename] == filename
1543 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1544 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1545 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1546 rotate_and_cleanup.call(angle)
1547 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1549 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1550 msg 3, "gesture delete: click-drag right button to the bottom"
1552 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1557 ctxt.targets.each { |target|
1558 if target.name == 'reorder-elements'
1559 move_dnd = proc { |from,to|
1562 autotable.move(from, to)
1563 save_undo(_("reorder"),
1566 autotable.move(to - 1, from)
1568 autotable.move(to, from + 1)
1570 $notebook.set_page(1)
1572 autotable.move(from, to)
1573 $notebook.set_page(1)
1578 if $multiple_dnd.size == 0
1579 move_dnd.call(selection_data.data.to_i,
1580 autotable.get_current_number(vbox))
1582 UndoHandler.begin_batch
1583 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1585 #- need to update current position between each call
1586 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1587 autotable.get_current_number(vbox))
1589 UndoHandler.end_batch
1600 def create_auto_table
1602 $autotable = Gtk::AutoTable.new(5)
1604 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1605 thumbnails_vb = Gtk::VBox.new(false, 5)
1607 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1608 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1609 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1610 thumbnails_vb.add($autotable)
1612 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1613 $autotable_sw.add_with_viewport(thumbnails_vb)
1615 #- follows stuff for handling multiple elements selection
1616 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1618 update_selected = proc {
1619 $autotable.current_order.each { |path|
1620 w = $name2widgets[path][:evtbox].window
1621 xm = w.position[0] + w.size[0]/2
1622 ym = w.position[1] + w.size[1]/2
1623 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1624 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1625 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1626 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1629 if $selected_elements[path] && ! $selected_elements[path][:keep]
1630 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))
1631 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1632 $selected_elements.delete(path)
1637 $autotable.signal_connect('realize') { |w,e|
1638 gc = Gdk::GC.new($autotable.window)
1639 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1640 gc.function = Gdk::GC::INVERT
1641 #- autoscroll handling for DND and multiple selections
1642 Gtk.timeout_add(100) {
1643 if ! $autotable.window.nil?
1644 w, x, y, mask = $autotable.window.pointer
1645 if mask & Gdk::Window::BUTTON1_MASK != 0
1646 if y < $autotable_sw.vadjustment.value
1648 $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]])
1650 if $button1_pressed_autotable || press_x
1651 scroll_upper($autotable_sw, y)
1654 w, pos_x, pos_y = $autotable.window.pointer
1655 $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]])
1656 update_selected.call
1659 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1661 $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]])
1663 if $button1_pressed_autotable || press_x
1664 scroll_lower($autotable_sw, y)
1667 w, pos_x, pos_y = $autotable.window.pointer
1668 $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]])
1669 update_selected.call
1674 ! $autotable.window.nil?
1678 $autotable.signal_connect('button-press-event') { |w,e|
1680 if !$button1_pressed_autotable
1683 if e.state & Gdk::Window::SHIFT_MASK == 0
1684 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1685 $selected_elements = {}
1686 $statusbar.push(0, utf8(_("Nothing selected.")))
1688 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1690 set_mousecursor(Gdk::Cursor::TCROSS)
1694 $autotable.signal_connect('button-release-event') { |w,e|
1696 if $button1_pressed_autotable
1697 #- unselect all only now
1698 $multiple_dnd = $selected_elements.keys
1699 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1700 $selected_elements = {}
1701 $button1_pressed_autotable = false
1704 $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]])
1705 if $selected_elements.length > 0
1706 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1709 press_x = press_y = pos_x = pos_y = nil
1710 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1714 $autotable.signal_connect('motion-notify-event') { |w,e|
1717 $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]])
1721 $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]])
1722 update_selected.call
1728 def create_subalbums_page
1730 subalbums_hb = Gtk::HBox.new
1731 $subalbums_vb = Gtk::VBox.new(false, 5)
1732 subalbums_hb.pack_start($subalbums_vb, false, false)
1733 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1734 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1735 $subalbums_sw.add_with_viewport(subalbums_hb)
1738 def save_current_file
1744 ios = File.open($filename, "w")
1745 $xmldoc.write(ios, 0)
1747 rescue Iconv::IllegalSequence
1748 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1749 if ! ios.nil? && ! ios.closed?
1752 $xmldoc.xml_decl.encoding = 'UTF-8'
1753 ios = File.open($filename, "w")
1754 $xmldoc.write(ios, 0)
1765 def save_current_file_user
1766 save_tempfilename = $filename
1767 $filename = $orig_filename
1768 if ! save_current_file
1769 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1770 $filename = save_tempfilename
1774 $generated_outofline = false
1775 $filename = save_tempfilename
1777 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1778 $todelete.each { |f|
1779 system("rm -f #{f}")
1783 def mark_document_as_dirty
1784 $xmldoc.elements.each('//dir') { |elem|
1785 elem.delete_attribute('already-generated')
1789 #- ret: true => ok false => cancel
1790 def ask_save_modifications(msg1, msg2, *options)
1792 options = options.size > 0 ? options[0] : {}
1794 if options[:disallow_cancel]
1795 dialog = Gtk::Dialog.new(msg1,
1797 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1798 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1799 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1801 dialog = Gtk::Dialog.new(msg1,
1803 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1804 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1805 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1806 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1808 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1809 dialog.vbox.add(Gtk::Label.new(msg2))
1810 dialog.window_position = Gtk::Window::POS_CENTER
1813 dialog.run { |response|
1815 if response == Gtk::Dialog::RESPONSE_YES
1816 if ! save_current_file_user
1817 return ask_save_modifications(msg1, msg2, options)
1820 #- if we have generated an album but won't save modifications, we must remove
1821 #- already-generated markers in original file
1822 if $generated_outofline
1824 $xmldoc = REXML::Document.new File.new($orig_filename)
1825 mark_document_as_dirty
1826 ios = File.open($orig_filename, "w")
1827 $xmldoc.write(ios, 0)
1830 puts "exception: #{$!}"
1834 if response == Gtk::Dialog::RESPONSE_CANCEL
1837 $todelete = [] #- unconditionally clear the list of images/videos to delete
1843 def try_quit(*options)
1844 if ask_save_modifications(utf8(_("Save before quitting?")),
1845 utf8(_("Do you want to save your changes before quitting?")),
1851 def show_popup(parent, msg, *options)
1852 dialog = Gtk::Dialog.new
1853 if options[0] && options[0][:title]
1854 dialog.title = options[0][:title]
1856 dialog.title = utf8(_("Booh message"))
1858 lbl = Gtk::Label.new
1859 if options[0] && options[0][:nomarkup]
1864 if options[0] && options[0][:centered]
1865 lbl.set_justify(Gtk::Justification::CENTER)
1867 if options[0] && options[0][:selectable]
1868 lbl.selectable = true
1870 if options[0] && options[0][:topwidget]
1871 dialog.vbox.add(options[0][:topwidget])
1873 if options[0] && options[0][:scrolled]
1874 sw = Gtk::ScrolledWindow.new(nil, nil)
1875 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1876 sw.add_with_viewport(lbl)
1878 dialog.set_default_size(500, 600)
1880 dialog.vbox.add(lbl)
1881 dialog.set_default_size(200, 120)
1883 if options[0] && options[0][:okcancel]
1884 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1886 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1888 if options[0] && options[0][:pos_centered]
1889 dialog.window_position = Gtk::Window::POS_CENTER
1891 dialog.window_position = Gtk::Window::POS_MOUSE
1894 if options[0] && options[0][:linkurl]
1895 linkbut = Gtk::Button.new('')
1896 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1897 linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1898 linkbut.relief = Gtk::RELIEF_NONE
1899 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1900 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1901 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1906 if !options[0] || !options[0][:not_transient]
1907 dialog.transient_for = parent
1908 dialog.run { |response|
1910 if options[0] && options[0][:okcancel]
1911 return response == Gtk::Dialog::RESPONSE_OK
1915 dialog.signal_connect('response') { dialog.destroy }
1919 def backend_wait_message(parent, msg, infopipe_path, mode)
1921 w.set_transient_for(parent)
1924 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1925 vb.pack_start(Gtk::Label.new(msg), false, false)
1927 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1928 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1929 if mode != 'one dir scan'
1930 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1932 if mode == 'web-album'
1933 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1934 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1936 vb.pack_start(Gtk::HSeparator.new, false, false)
1938 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1939 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1940 vb.pack_end(bottom, false, false)
1942 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1943 refresh_thread = Thread.new {
1944 directories_counter = 0
1945 while line = infopipe.gets
1946 if line =~ /^directories: (\d+), sizes: (\d+)/
1947 directories = $1.to_f + 1
1949 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1950 elements = $3.to_f + 1
1951 if mode == 'web-album'
1955 gtk_thread_protect { pb1_1.fraction = 0 }
1956 if mode != 'one dir scan'
1957 newtext = utf8(full_src_dir_to_rel($1, $2))
1958 newtext = '/' if newtext == ''
1959 gtk_thread_protect { pb1_2.text = newtext }
1960 directories_counter += 1
1961 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1963 elsif line =~ /^processing element$/
1964 element_counter += 1
1965 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1966 elsif line =~ /^processing size$/
1967 element_counter += 1
1968 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1969 elsif line =~ /^finished processing sizes$/
1970 gtk_thread_protect { pb1_1.fraction = 1 }
1971 elsif line =~ /^creating index.html$/
1972 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1973 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1974 directories_counter = 0
1975 elsif line =~ /^index.html: (.+)\|(.+)/
1976 newtext = utf8(full_src_dir_to_rel($1, $2))
1977 newtext = '/' if newtext == ''
1978 gtk_thread_protect { pb2.text = newtext }
1979 directories_counter += 1
1980 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1981 elsif line =~ /^die: (.*)$/
1988 w.signal_connect('delete-event') { w.destroy }
1989 w.signal_connect('destroy') {
1990 Thread.kill(refresh_thread)
1991 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1994 system("rm -f #{infopipe_path}")
1997 w.window_position = Gtk::Window::POS_CENTER
2003 def call_backend(cmd, waitmsg, mode, params)
2004 pipe = Tempfile.new("boohpipe")
2006 system("mkfifo #{pipe.path}")
2007 cmd += " --info-pipe #{pipe.path}"
2008 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2013 id, exitstatus = Process.waitpid2(pid)
2014 gtk_thread_protect { w8.destroy }
2016 if params[:successmsg]
2017 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2019 if params[:closure_after]
2020 gtk_thread_protect(¶ms[:closure_after])
2022 elsif exitstatus == 15
2023 #- say nothing, user aborted
2025 gtk_thread_protect { show_popup($main_window,
2026 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2032 button.signal_connect('clicked') {
2033 Process.kill('SIGTERM', pid)
2037 def save_changes(*forced)
2038 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2042 $xmldir.delete_attribute('already-generated')
2044 propagate_children = proc { |xmldir|
2045 if xmldir.attributes['subdirs-caption']
2046 xmldir.delete_attribute('already-generated')
2048 xmldir.elements.each('dir') { |element|
2049 propagate_children.call(element)
2053 if $xmldir.child_byname_notattr('dir', 'deleted')
2054 new_title = $subalbums_title.buffer.text
2055 if new_title != $xmldir.attributes['subdirs-caption']
2056 parent = $xmldir.parent
2057 if parent.name == 'dir'
2058 parent.delete_attribute('already-generated')
2060 propagate_children.call($xmldir)
2062 $xmldir.add_attribute('subdirs-caption', new_title)
2063 $xmldir.elements.each('dir') { |element|
2064 if !element.attributes['deleted']
2065 path = element.attributes['path']
2066 newtext = $subalbums_edits[path][:editzone].buffer.text
2067 if element.attributes['subdirs-caption']
2068 if element.attributes['subdirs-caption'] != newtext
2069 propagate_children.call(element)
2071 element.add_attribute('subdirs-caption', newtext)
2072 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2074 if element.attributes['thumbnails-caption'] != newtext
2075 element.delete_attribute('already-generated')
2077 element.add_attribute('thumbnails-caption', newtext)
2078 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2084 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2085 if $xmldir.attributes['thumbnails-caption']
2086 path = $xmldir.attributes['path']
2087 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2089 elsif $xmldir.attributes['thumbnails-caption']
2090 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2093 if $xmldir.attributes['thumbnails-caption']
2094 if edit = $subalbums_edits[$xmldir.attributes['path']]
2095 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2099 #- remove and reinsert elements to reflect new ordering
2102 $xmldir.elements.each { |element|
2103 if element.name == 'image' || element.name == 'video'
2104 saves[element.attributes['filename']] = element.remove
2108 $autotable.current_order.each { |path|
2109 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2110 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2113 saves.each_key { |path|
2114 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2115 chld.add_attribute('deleted', 'true')
2119 def sort_by_exif_date
2123 $xmldir.elements.each { |element|
2124 if element.name == 'image' || element.name == 'video'
2125 current_order << element.attributes['filename']
2129 #- look for EXIF dates
2131 w.set_transient_for($main_window)
2133 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2134 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2135 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2136 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2137 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2138 vb.pack_end(bottom, false, false)
2140 w.signal_connect('delete-event') { w.destroy }
2141 w.window_position = Gtk::Window::POS_CENTER
2145 b.signal_connect('clicked') { aborted = true }
2148 current_order.each { |f|
2150 if entry2type(f) == 'image'
2152 pb.fraction = i.to_f / current_order.size
2153 Gtk.main_iteration while Gtk.events_pending?
2154 date_time = `identify -format "%[EXIF:DateTimeOriginal]" '#{from_utf8($current_path + "/" + f)}'`.chomp
2155 if $? == 0 && date_time != ''
2156 dates[f] = date_time
2169 $xmldir.elements.each { |element|
2170 if element.name == 'image' || element.name == 'video'
2171 saves[element.attributes['filename']] = element.remove
2175 #- find a good fallback for all entries without a date (still next to the item they were next to)
2176 neworder = dates.keys.sort { |a,b| dates[a] <=> dates[b] }
2177 for i in 0 .. current_order.size - 1
2178 if ! neworder.include?(current_order[i])
2180 while j > 0 && ! neworder.include?(current_order[j])
2183 neworder[(neworder.index(current_order[j]) || -1 ) + 1, 0] = current_order[i]
2187 $xmldir.add_element(saves[f].name, saves[f].attributes)
2190 #- let the auto-table reflect new ordering
2194 def remove_all_captions
2197 $autotable.current_order.each { |path|
2198 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2199 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2201 save_undo(_("remove all captions"),
2203 texts.each_key { |key|
2204 $name2widgets[key][:textview].buffer.text = texts[key]
2206 $notebook.set_page(1)
2208 texts.each_key { |key|
2209 $name2widgets[key][:textview].buffer.text = ''
2211 $notebook.set_page(1)
2217 $selected_elements.each_key { |path|
2218 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2224 $selected_elements = {}
2228 $undo_tb.sensitive = $undo_mb.sensitive = false
2229 $redo_tb.sensitive = $redo_mb.sensitive = false
2235 $subalbums_vb.children.each { |chld|
2236 $subalbums_vb.remove(chld)
2238 $subalbums = Gtk::Table.new(0, 0, true)
2239 current_y_sub_albums = 0
2241 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2242 $subalbums_edits = {}
2243 subalbums_counter = 0
2244 subalbums_edits_bypos = {}
2246 add_subalbum = proc { |xmldir, counter|
2247 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2248 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2249 if xmldir == $xmldir
2250 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2251 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2252 caption = xmldir.attributes['thumbnails-caption']
2253 infotype = 'thumbnails'
2255 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2256 captionfile, caption = find_subalbum_caption_info(xmldir)
2257 infotype = find_subalbum_info_type(xmldir)
2259 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2260 hbox = Gtk::HBox.new
2261 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2263 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2266 my_gen_real_thumbnail = proc {
2267 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2270 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2271 f.add(img = Gtk::Image.new)
2272 my_gen_real_thumbnail.call
2274 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2276 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2277 $subalbums.attach(hbox,
2278 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2280 frame, textview = create_editzone($subalbums_sw, 0, img)
2281 textview.buffer.text = caption
2282 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2283 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2285 change_image = proc {
2286 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2288 Gtk::FileChooser::ACTION_OPEN,
2290 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2291 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2292 fc.transient_for = $main_window
2293 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))
2294 f.add(preview_img = Gtk::Image.new)
2296 fc.signal_connect('update-preview') { |w|
2298 if fc.preview_filename
2299 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2300 fc.preview_widget_active = true
2302 rescue Gdk::PixbufError
2303 fc.preview_widget_active = false
2306 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2308 old_file = captionfile
2309 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2310 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2311 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2312 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2314 new_file = fc.filename
2315 msg 3, "new captionfile is: #{fc.filename}"
2316 perform_changefile = proc {
2317 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2318 $modified_pixbufs.delete(thumbnail_file)
2319 xmldir.delete_attribute("#{infotype}-rotate")
2320 xmldir.delete_attribute("#{infotype}-color-swap")
2321 xmldir.delete_attribute("#{infotype}-enhance")
2322 xmldir.delete_attribute("#{infotype}-frame-offset")
2323 my_gen_real_thumbnail.call
2325 perform_changefile.call
2327 save_undo(_("change caption file for sub-album"),
2329 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2330 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2331 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2332 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2333 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2334 my_gen_real_thumbnail.call
2335 $notebook.set_page(0)
2337 perform_changefile.call
2338 $notebook.set_page(0)
2346 system("rm -f '#{thumbnail_file}'")
2347 my_gen_real_thumbnail.call
2350 rotate_and_cleanup = proc { |angle|
2351 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2352 system("rm -f '#{thumbnail_file}'")
2355 move = proc { |direction|
2358 save_changes('forced')
2359 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2360 if direction == 'up'
2361 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2362 subalbums_edits_bypos[oldpos - 1][:position] += 1
2364 if direction == 'down'
2365 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2366 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2368 if direction == 'top'
2369 for i in 1 .. oldpos - 1
2370 subalbums_edits_bypos[i][:position] += 1
2372 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2374 if direction == 'bottom'
2375 for i in oldpos + 1 .. subalbums_counter
2376 subalbums_edits_bypos[i][:position] -= 1
2378 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2382 $xmldir.elements.each('dir') { |element|
2383 if (!element.attributes['deleted'])
2384 elems << [ element.attributes['path'], element.remove ]
2387 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2388 each { |e| $xmldir.add_element(e[1]) }
2389 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2390 $xmldir.elements.each('descendant::dir') { |elem|
2391 elem.delete_attribute('already-generated')
2394 sel = $albums_tv.selection.selected_rows
2396 populate_subalbums_treeview(false)
2397 $albums_tv.selection.select_path(sel[0])
2400 color_swap_and_cleanup = proc {
2401 perform_color_swap_and_cleanup = proc {
2402 color_swap(xmldir, "#{infotype}-")
2403 my_gen_real_thumbnail.call
2405 perform_color_swap_and_cleanup.call
2407 save_undo(_("color swap"),
2409 perform_color_swap_and_cleanup.call
2410 $notebook.set_page(0)
2412 perform_color_swap_and_cleanup.call
2413 $notebook.set_page(0)
2418 change_frame_offset_and_cleanup = proc {
2419 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2420 perform_change_frame_offset_and_cleanup = proc { |val|
2421 change_frame_offset(xmldir, "#{infotype}-", val)
2422 my_gen_real_thumbnail.call
2424 perform_change_frame_offset_and_cleanup.call(values[:new])
2426 save_undo(_("specify frame offset"),
2428 perform_change_frame_offset_and_cleanup.call(values[:old])
2429 $notebook.set_page(0)
2431 perform_change_frame_offset_and_cleanup.call(values[:new])
2432 $notebook.set_page(0)
2438 whitebalance_and_cleanup = proc {
2439 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2440 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2441 perform_change_whitebalance_and_cleanup = proc { |val|
2442 change_whitebalance(xmldir, "#{infotype}-", val)
2443 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2444 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2445 system("rm -f '#{thumbnail_file}'")
2447 perform_change_whitebalance_and_cleanup.call(values[:new])
2449 save_undo(_("fix white balance"),
2451 perform_change_whitebalance_and_cleanup.call(values[:old])
2452 $notebook.set_page(0)
2454 perform_change_whitebalance_and_cleanup.call(values[:new])
2455 $notebook.set_page(0)
2461 gammacorrect_and_cleanup = proc {
2462 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2463 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2464 perform_change_gammacorrect_and_cleanup = proc { |val|
2465 change_gammacorrect(xmldir, "#{infotype}-", val)
2466 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2467 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2468 system("rm -f '#{thumbnail_file}'")
2470 perform_change_gammacorrect_and_cleanup.call(values[:new])
2472 save_undo(_("gamma correction"),
2474 perform_change_gammacorrect_and_cleanup.call(values[:old])
2475 $notebook.set_page(0)
2477 perform_change_gammacorrect_and_cleanup.call(values[:new])
2478 $notebook.set_page(0)
2484 enhance_and_cleanup = proc {
2485 perform_enhance_and_cleanup = proc {
2486 enhance(xmldir, "#{infotype}-")
2487 my_gen_real_thumbnail.call
2490 perform_enhance_and_cleanup.call
2492 save_undo(_("enhance"),
2494 perform_enhance_and_cleanup.call
2495 $notebook.set_page(0)
2497 perform_enhance_and_cleanup.call
2498 $notebook.set_page(0)
2503 evtbox.signal_connect('button-press-event') { |w, event|
2504 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2506 rotate_and_cleanup.call(90)
2508 rotate_and_cleanup.call(-90)
2509 elsif $enhance.active?
2510 enhance_and_cleanup.call
2513 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2514 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2515 { :forbid_left => true, :forbid_right => true,
2516 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2517 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2518 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2519 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2520 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2522 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2527 evtbox.signal_connect('button-press-event') { |w, event|
2528 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2532 evtbox.signal_connect('button-release-event') { |w, event|
2533 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2534 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2535 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2536 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2537 msg 3, "gesture rotate: #{angle}"
2538 rotate_and_cleanup.call(angle)
2541 $gesture_press = nil
2544 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2545 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2546 current_y_sub_albums += 1
2549 if $xmldir.child_byname_notattr('dir', 'deleted')
2551 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2552 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2553 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2554 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2555 #- this album image/caption
2556 if $xmldir.attributes['thumbnails-caption']
2557 add_subalbum.call($xmldir, 0)
2560 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2561 $xmldir.elements.each { |element|
2562 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2563 #- element (image or video) of this album
2564 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2565 msg 3, "dest_img: #{dest_img}"
2566 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2567 total[element.name] += 1
2569 if element.name == 'dir' && !element.attributes['deleted']
2570 #- sub-album image/caption
2571 add_subalbum.call(element, subalbums_counter += 1)
2572 total[element.name] += 1
2575 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2576 total['image'], total['video'], total['dir'] ]))
2577 $subalbums_vb.add($subalbums)
2578 $subalbums_vb.show_all
2580 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2581 $notebook.get_tab_label($autotable_sw).sensitive = false
2582 $notebook.set_page(0)
2583 $thumbnails_title.buffer.text = ''
2585 $notebook.get_tab_label($autotable_sw).sensitive = true
2586 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2589 if !$xmldir.child_byname_notattr('dir', 'deleted')
2590 $notebook.get_tab_label($subalbums_sw).sensitive = false
2591 $notebook.set_page(1)
2593 $notebook.get_tab_label($subalbums_sw).sensitive = true
2597 def pixbuf_or_nil(filename)
2599 return Gdk::Pixbuf.new(filename)
2605 def theme_choose(current)
2606 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2608 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2609 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2610 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2612 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2613 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2614 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2615 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2616 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2617 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2618 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2619 treeview.signal_connect('button-press-event') { |w, event|
2620 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2621 dialog.response(Gtk::Dialog::RESPONSE_OK)
2625 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2627 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2630 iter[0] = File.basename(dir)
2631 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2632 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2633 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2634 if File.basename(dir) == current
2635 treeview.selection.select_iter(iter)
2639 dialog.set_default_size(700, 400)
2640 dialog.vbox.show_all
2641 dialog.run { |response|
2642 iter = treeview.selection.selected
2644 if response == Gtk::Dialog::RESPONSE_OK && iter
2645 return model.get_value(iter, 0)
2651 def show_password_protections
2652 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2653 child_iter = $albums_iters[xmldir.attributes['path']]
2654 if xmldir.attributes['password-protect']
2655 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2656 already_protected = true
2657 elsif already_protected
2658 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2660 pix = pix.saturate_and_pixelate(1, true)
2666 xmldir.elements.each('dir') { |elem|
2667 if !elem.attributes['deleted']
2668 examine_dir_elem.call(child_iter, elem, already_protected)
2672 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2675 def populate_subalbums_treeview(select_first)
2679 $subalbums_vb.children.each { |chld|
2680 $subalbums_vb.remove(chld)
2683 source = $xmldoc.root.attributes['source']
2684 msg 3, "source: #{source}"
2686 xmldir = $xmldoc.elements['//dir']
2687 if !xmldir || xmldir.attributes['path'] != source
2688 msg 1, _("Corrupted booh file...")
2692 append_dir_elem = proc { |parent_iter, xmldir|
2693 child_iter = $albums_ts.append(parent_iter)
2694 child_iter[0] = File.basename(xmldir.attributes['path'])
2695 child_iter[1] = xmldir.attributes['path']
2696 $albums_iters[xmldir.attributes['path']] = child_iter
2697 msg 3, "puttin location: #{xmldir.attributes['path']}"
2698 xmldir.elements.each('dir') { |elem|
2699 if !elem.attributes['deleted']
2700 append_dir_elem.call(child_iter, elem)
2704 append_dir_elem.call(nil, xmldir)
2705 show_password_protections
2707 $albums_tv.expand_all
2709 $albums_tv.selection.select_iter($albums_ts.iter_first)
2713 def select_current_theme
2714 select_theme($xmldoc.root.attributes['theme'],
2715 $xmldoc.root.attributes['limit-sizes'],
2716 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2717 $xmldoc.root.attributes['thumbnails-per-row'])
2720 def open_file(filename)
2724 $current_path = nil #- invalidate
2725 $modified_pixbufs = {}
2728 $subalbums_vb.children.each { |chld|
2729 $subalbums_vb.remove(chld)
2732 if !File.exists?(filename)
2733 return utf8(_("File not found."))
2737 $xmldoc = REXML::Document.new File.new(filename)
2742 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2743 if entry2type(filename).nil?
2744 return utf8(_("Not a booh file!"))
2746 return utf8(_("Not a booh file!\n\nHint: you cannot import directly an image or video with File/Open.\nUse File/New to create a new album."))
2750 if !source = $xmldoc.root.attributes['source']
2751 return utf8(_("Corrupted booh file..."))
2754 if !dest = $xmldoc.root.attributes['destination']
2755 return utf8(_("Corrupted booh file..."))
2758 if !theme = $xmldoc.root.attributes['theme']
2759 return utf8(_("Corrupted booh file..."))
2762 if $xmldoc.root.attributes['version'] < '0.8.6'
2763 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2764 mark_document_as_dirty
2765 if $xmldoc.root.attributes['version'] < '0.8.4'
2766 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2767 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2768 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2769 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2770 if old_dest_dir != new_dest_dir
2771 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2773 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2774 xmldir.elements.each { |element|
2775 if %w(image video).include?(element.name) && !element.attributes['deleted']
2776 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2777 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2778 Dir[old_name + '*'].each { |file|
2779 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2780 file != new_file and sys("mv '#{file}' '#{new_file}'")
2783 if element.name == 'dir' && !element.attributes['deleted']
2784 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2785 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2786 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2790 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2794 $xmldoc.root.add_attribute('version', $VERSION)
2797 select_current_theme
2799 $filename = filename
2800 $default_size['thumbnails'] =~ /(.*)x(.*)/
2801 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2802 $albums_thumbnail_size =~ /(.*)x(.*)/
2803 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2805 populate_subalbums_treeview(true)
2807 $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
2811 def open_file_user(filename)
2812 result = open_file(filename)
2814 $config['last-opens'] ||= []
2815 if $config['last-opens'][-1] != utf8(filename)
2816 $config['last-opens'] << utf8(filename)
2818 $orig_filename = $filename
2819 tmp = Tempfile.new("boohtemp")
2822 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2824 $tempfiles << $filename << "#{$filename}.backup"
2826 $orig_filename = nil
2832 if !ask_save_modifications(utf8(_("Save this album?")),
2833 utf8(_("Do you want to save the changes to this album?")),
2834 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2837 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2839 Gtk::FileChooser::ACTION_OPEN,
2841 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2842 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2843 fc.set_current_folder(File.expand_path("~/.booh"))
2844 fc.transient_for = $main_window
2847 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2848 push_mousecursor_wait(fc)
2849 msg = open_file_user(fc.filename)
2865 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2870 def additional_booh_options
2873 options += "--mproc #{$config['mproc'].to_i} "
2875 options += "--comments-format '#{$config['comments-format']}'"
2880 if !ask_save_modifications(utf8(_("Save this album?")),
2881 utf8(_("Do you want to save the changes to this album?")),
2882 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2885 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2887 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2888 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2889 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2891 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2892 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2893 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2894 tbl.attach(src = Gtk::Entry.new,
2895 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2896 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2897 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2898 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2899 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2900 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2901 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2902 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2903 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2904 tbl.attach(dest = Gtk::Entry.new,
2905 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2906 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2907 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2908 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2909 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2910 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2911 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2912 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2913 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2915 tooltips = Gtk::Tooltips.new
2916 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2917 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2918 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2919 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2920 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2921 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2922 tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of pictures from non digital cameras are 3/2 when pictures from digital cameras are 4/3)")), nil)
2923 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2924 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2925 nperpage_model = Gtk::ListStore.new(String, String)
2926 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
2927 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
2928 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
2929 nperpagecombo.set_attributes(crt, { :markup => 0 })
2930 iter = nperpage_model.append
2931 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
2933 [ 12, 20, 30, 40, 50 ].each { |v|
2934 iter = nperpage_model.append
2935 iter[0] = iter[1] = v.to_s
2937 nperpagecombo.active = 0
2938 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
2939 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
2940 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)
2941 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2942 pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
2943 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)
2945 src_nb_calculated_for = ''
2947 process_src_nb = proc {
2948 if src.text != src_nb_calculated_for
2949 src_nb_calculated_for = src.text
2951 Thread.kill(src_nb_thread)
2954 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
2955 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
2957 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2958 if File.readable?(from_utf8_safe(src_nb_calculated_for))
2959 src_nb_thread = Thread.new {
2960 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2961 total = { 'image' => 0, 'video' => 0, nil => 0 }
2962 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2963 if File.basename(dir) =~ /^\./
2967 Dir.entries(dir.chomp).each { |file|
2968 total[entry2type(file)] += 1
2970 rescue Errno::EACCES, Errno::ENOENT
2974 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2978 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2981 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2987 timeout_src_nb = Gtk.timeout_add(100) {
2991 src_browse.signal_connect('clicked') {
2992 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2994 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2996 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2997 fc.transient_for = $main_window
2998 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2999 src.text = utf8(fc.filename)
3001 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3006 dest_browse.signal_connect('clicked') {
3007 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3009 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3011 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3012 fc.transient_for = $main_window
3013 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3014 dest.text = utf8(fc.filename)
3019 conf_browse.signal_connect('clicked') {
3020 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3022 Gtk::FileChooser::ACTION_SAVE,
3024 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3025 fc.transient_for = $main_window
3026 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3027 fc.set_current_folder(File.expand_path("~/.booh"))
3028 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3029 conf.text = utf8(fc.filename)
3036 recreate_theme_config = proc {
3037 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3039 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3040 $images_size.each { |s|
3041 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3045 tooltips.set_tip(cb, utf8(s['description']), nil)
3046 theme_sizes << { :widget => cb, :value => s['name'] }
3048 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3049 tooltips = Gtk::Tooltips.new
3050 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3051 theme_sizes << { :widget => cb, :value => 'original' }
3054 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3057 $allowed_N_values.each { |n|
3059 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3061 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3063 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3067 nperrows << { :widget => rb, :value => n }
3069 nperrowradios.show_all
3071 recreate_theme_config.call
3073 theme_button.signal_connect('clicked') {
3074 if newtheme = theme_choose(theme_button.label)
3075 theme_button.label = newtheme
3076 recreate_theme_config.call
3080 dialog.vbox.add(frame1)
3081 dialog.vbox.add(frame2)
3087 dialog.run { |response|
3088 if response == Gtk::Dialog::RESPONSE_OK
3089 srcdir = from_utf8_safe(src.text)
3090 destdir = from_utf8_safe(dest.text)
3091 confpath = from_utf8_safe(conf.text)
3092 if src.text != '' && srcdir == ''
3093 show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3095 elsif !File.directory?(srcdir)
3096 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3098 elsif dest.text != '' && destdir == ''
3099 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3101 elsif destdir != make_dest_filename(destdir)
3102 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3104 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3105 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
3107 elsif File.exists?(destdir) && !File.directory?(destdir)
3108 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3110 elsif conf.text == ''
3111 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3113 elsif conf.text != '' && confpath == ''
3114 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3116 elsif File.directory?(confpath)
3117 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3119 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3120 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3122 system("mkdir '#{destdir}'")
3123 if !File.directory?(destdir)
3124 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3136 srcdir = from_utf8(src.text)
3137 destdir = from_utf8(dest.text)
3138 configskel = File.expand_path(from_utf8(conf.text))
3139 theme = theme_button.label
3140 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3141 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3142 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3143 opt432 = optimize432.active?
3144 madewith = madewithentry.text
3145 indexlink = indexlinkentry.text
3148 Thread.kill(src_nb_thread)
3149 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3152 Gtk.timeout_remove(timeout_src_nb)
3155 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3156 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3157 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3158 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3159 utf8(_("Please wait while scanning source directory...")),
3161 { :closure_after => proc { open_file_user(configskel) } })
3166 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3168 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3169 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3170 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3172 source = $xmldoc.root.attributes['source']
3173 dest = $xmldoc.root.attributes['destination']
3174 theme = $xmldoc.root.attributes['theme']
3175 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3176 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3177 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3178 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3180 limit_sizes = limit_sizes.split(/,/)
3182 madewith = $xmldoc.root.attributes['made-with']
3183 indexlink = $xmldoc.root.attributes['index-link']
3185 tooltips = Gtk::Tooltips.new
3186 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3187 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3188 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3189 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3190 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3191 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3192 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3193 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3194 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3195 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3196 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3197 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3198 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3200 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3201 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3202 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3203 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3204 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3205 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3206 tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of pictures from non digital cameras are 3/2 when pictures from digital cameras are 4/3)")), nil)
3207 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3208 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3209 nperpage_model = Gtk::ListStore.new(String, String)
3210 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3211 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3212 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3213 nperpagecombo.set_attributes(crt, { :markup => 0 })
3214 iter = nperpage_model.append
3215 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3217 [ 12, 20, 30, 40, 50 ].each { |v|
3218 iter = nperpage_model.append
3219 iter[0] = iter[1] = v.to_s
3220 if nperpage && nperpage == v.to_s
3221 nperpagecombo.active_iter = iter
3224 if nperpagecombo.active_iter.nil?
3225 nperpagecombo.active = 0
3228 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3229 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3231 indexlinkentry.text = indexlink
3233 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)
3234 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3235 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3237 madewithentry.text = madewith
3239 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)
3243 recreate_theme_config = proc {
3244 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3246 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3248 $images_size.each { |s|
3249 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3251 if limit_sizes.include?(s['name'])
3259 tooltips.set_tip(cb, utf8(s['description']), nil)
3260 theme_sizes << { :widget => cb, :value => s['name'] }
3262 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3263 tooltips = Gtk::Tooltips.new
3264 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3265 if limit_sizes && limit_sizes.include?('original')
3268 theme_sizes << { :widget => cb, :value => 'original' }
3271 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3274 $allowed_N_values.each { |n|
3276 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3278 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3280 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3281 nperrowradios.add(Gtk::Label.new(' '))
3282 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3285 nperrows << { :widget => rb, :value => n.to_s }
3287 nperrowradios.show_all
3289 recreate_theme_config.call
3291 theme_button.signal_connect('clicked') {
3292 if newtheme = theme_choose(theme_button.label)
3295 theme_button.label = newtheme
3296 recreate_theme_config.call
3300 dialog.vbox.add(frame1)
3301 dialog.vbox.add(frame2)
3307 dialog.run { |response|
3308 if response == Gtk::Dialog::RESPONSE_OK
3309 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3310 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3319 save_theme = theme_button.label
3320 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3321 save_opt432 = optimize432.active?
3322 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3323 save_nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3324 save_madewith = madewithentry.text
3325 save_indexlink = indexlinkentry.text
3328 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_nperpage != nperpage || save_madewith != madewith || save_indexlink != indexlinkentry)
3329 mark_document_as_dirty
3331 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3332 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3333 (save_nperpage ? "--thumbnails-per-page #{save_nperpage} " : '') +
3334 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3335 utf8(_("Please wait while scanning source directory...")),
3337 { :closure_after => proc {
3338 open_file($filename)
3342 #- select_theme merges global variables, need to return to current choices
3343 select_current_theme
3350 sel = $albums_tv.selection.selected_rows
3352 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3353 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3354 utf8(_("Please wait while scanning source directory...")),
3356 { :closure_after => proc {
3357 open_file($filename)
3358 $albums_tv.selection.select_path(sel[0])
3366 sel = $albums_tv.selection.selected_rows
3368 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3369 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3370 utf8(_("Please wait while scanning source directory...")),
3372 { :closure_after => proc {
3373 open_file($filename)
3374 $albums_tv.selection.select_path(sel[0])
3382 theme = $xmldoc.root.attributes['theme']
3383 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3385 limit_sizes = "--sizes #{limit_sizes}"
3387 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3388 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3389 utf8(_("Please wait while scanning source directory...")),
3391 { :closure_after => proc {
3392 open_file($filename)
3398 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3400 Gtk::FileChooser::ACTION_SAVE,
3402 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3403 fc.transient_for = $main_window
3404 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3405 fc.set_current_folder(File.expand_path("~/.booh"))
3406 fc.filename = $orig_filename
3407 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3408 $orig_filename = fc.filename
3409 if ! save_current_file_user
3413 $config['last-opens'] ||= []
3414 $config['last-opens'] << $orig_filename
3420 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3422 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3423 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3424 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3426 dialog.vbox.add(notebook = Gtk::Notebook.new)
3427 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3428 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3429 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3430 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer']).set_size_request(250, -1)),
3431 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3432 tooltips = Gtk::Tooltips.new
3433 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3434 for example: /usr/bin/mplayer %f")), nil)
3435 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3436 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3437 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3438 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3439 tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3440 for example: /usr/bin/gimp-remote %f")), nil)
3441 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3442 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3443 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3444 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3445 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3446 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3447 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3448 0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3449 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(smp_hbox = Gtk::HBox.new.add(smp_spin = Gtk::SpinButton.new(2, 16, 1)).add(Gtk::Label.new(utf8(_("processors")))).set_sensitive(false)),
3450 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3451 tooltips.set_tip(smp_check, utf8(_("When activated, this option allows the thumbnails creation to run faster. However, if you don't have a multi-processor machine, this will only slow down processing!")), nil)
3452 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3453 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3454 tooltips.set_tip(nogestures_check, utf8(_("Mouse gestures are 'unusual' mouse movements triggering special actions, and are great for speeding up your editions. Get details on available mouse gestures from the Help menu.")), nil)
3455 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3456 0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3457 tooltips.set_tip(deleteondisk_check, utf8(_("Normally, deleting an image or video in booh only removes it from the web-album. If you check this option, the original file in source directory will be removed as well. Undo is possible, since actual deletion is performed only when web-album is saved.")), nil)
3459 smp_check.signal_connect('toggled') {
3460 if smp_check.active?
3461 smp_hbox.sensitive = true
3463 smp_hbox.sensitive = false
3467 smp_check.active = true
3468 smp_spin.value = $config['mproc'].to_i
3470 nogestures_check.active = $config['nogestures']
3471 deleteondisk_check.active = $config['deleteondisk']
3473 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3474 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3475 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3476 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3477 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3478 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3479 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3480 tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3481 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3482 tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3483 2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3484 tooltips.set_tip(commentsformat_entry, utf8(_("Normally, filenames without extension are used as comments for images and videos in new albums. Use this entry to use something else for images.")), nil)
3485 commentsformat_help.signal_connect('clicked') {
3486 show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3487 hence you should look at ImageMagick/identify documentation for the most
3488 accurate and up-to-date documentation. Last time I checked, documentation
3491 Print information about the image in a format of your choosing. You can
3492 include the image filename, type, width, height, Exif data, or other image
3493 attributes by embedding special format characters:
3496 %P page width and height