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 dialog.window_position = Gtk::Window::POS_MOUSE
594 spin.value = value.to_f
601 dialog.run { |response|
605 newval = spin.value.to_f
608 if response == Gtk::Dialog::RESPONSE_OK
610 msg 3, "changing panorama amount to #{newval}"
611 return { :old => value, :new => newval }
618 def change_whitebalance(xmlelem, attributes_prefix, value)
620 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
623 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
625 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
626 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
627 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
628 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
629 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
630 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
631 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
632 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
633 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
634 $modified_pixbufs[thumbnail_img] ||= {}
635 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
636 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
638 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
639 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
641 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
644 $modified_pixbufs[thumbnail_img] ||= {}
645 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
647 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
650 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
651 #- init $modified_pixbufs correctly
652 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
654 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
656 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
658 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
659 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
660 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
664 _("You can fix the <b>white balance</b> of the image, if your image is too blue
665 or too yellow because your camera didn't detect the light correctly. Drag the
666 slider below the image to the left for more blue, to the right for more yellow.
670 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
672 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
674 dialog.window_position = Gtk::Window::POS_MOUSE
678 timeout = Gtk.timeout_add(100) {
679 if hs.value != lastval
682 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
688 dialog.run { |response|
689 Gtk.timeout_remove(timeout)
690 if response == Gtk::Dialog::RESPONSE_OK
692 newval = hs.value.to_s
693 msg 3, "changing white balance to #{newval}"
695 return { :old => value, :new => newval }
698 $modified_pixbufs[thumbnail_img] ||= {}
699 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
700 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
708 def change_gammacorrect(xmlelem, attributes_prefix, value)
710 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
713 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
715 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
716 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
717 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
718 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
719 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
720 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
721 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
722 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
723 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
724 $modified_pixbufs[thumbnail_img] ||= {}
725 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
726 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
728 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
729 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
731 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
734 $modified_pixbufs[thumbnail_img] ||= {}
735 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
737 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
740 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
741 #- init $modified_pixbufs correctly
742 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
744 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
746 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
748 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
749 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
750 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
754 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
755 or too bright. Drag the slider below the image.
759 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
761 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
763 dialog.window_position = Gtk::Window::POS_MOUSE
767 timeout = Gtk.timeout_add(100) {
768 if hs.value != lastval
771 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
777 dialog.run { |response|
778 Gtk.timeout_remove(timeout)
779 if response == Gtk::Dialog::RESPONSE_OK
781 newval = hs.value.to_s
782 msg 3, "gamma correction to #{newval}"
784 return { :old => value, :new => newval }
787 $modified_pixbufs[thumbnail_img] ||= {}
788 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
789 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
797 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
798 system("rm -f '#{destfile}'")
799 #- type can be 'element' or 'subdir'
801 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
803 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
807 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
809 push_mousecursor_wait
810 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
813 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
819 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
820 distribute_multiple_call = Proc.new { |action, arg|
821 $selected_elements.each_key { |path|
822 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
824 if possible_actions[:can_multiple] && $selected_elements.length > 0
825 UndoHandler.begin_batch
826 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
827 UndoHandler.end_batch
829 closures[action].call(arg)
831 $selected_elements = {}
834 if optionals.include?('change_image')
835 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
836 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
837 changeimg.signal_connect('activate') { closures[:change].call }
838 menu.append(Gtk::SeparatorMenuItem.new)
840 if !possible_actions[:can_multiple] || $selected_elements.length == 0
843 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
844 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
845 view.signal_connect('activate') { closures[:view].call }
847 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
848 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
849 view.signal_connect('activate') { closures[:view].call }
850 menu.append(Gtk::SeparatorMenuItem.new)
853 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
854 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
855 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
856 exif.signal_connect('activate') { show_popup($main_window,
857 utf8(`identify -format "%[EXIF:*]" #{fullpath}`.sub(/MakerNote.*\n/, '')),
858 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
859 menu.append(Gtk::SeparatorMenuItem.new)
862 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
863 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
864 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
865 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
866 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
867 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
868 if !possible_actions[:can_multiple] || $selected_elements.length == 0
869 menu.append(Gtk::SeparatorMenuItem.new)
870 if !possible_actions[:forbid_left]
871 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
872 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
873 moveleft.signal_connect('activate') { closures[:move].call('left') }
874 if !possible_actions[:can_left]
875 moveleft.sensitive = false
878 if !possible_actions[:forbid_right]
879 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
880 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
881 moveright.signal_connect('activate') { closures[:move].call('right') }
882 if !possible_actions[:can_right]
883 moveright.sensitive = false
886 if optionals.include?('move_top')
887 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
888 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
889 movetop.signal_connect('activate') { closures[:move].call('top') }
890 if !possible_actions[:can_top]
891 movetop.sensitive = false
894 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
895 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
896 moveup.signal_connect('activate') { closures[:move].call('up') }
897 if !possible_actions[:can_up]
898 moveup.sensitive = false
900 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
901 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
902 movedown.signal_connect('activate') { closures[:move].call('down') }
903 if !possible_actions[:can_down]
904 movedown.sensitive = false
906 if optionals.include?('move_bottom')
907 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
908 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
909 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
910 if !possible_actions[:can_bottom]
911 movebottom.sensitive = false
916 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
917 menu.append(Gtk::SeparatorMenuItem.new)
918 menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
919 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
920 color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
921 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
922 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
923 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
924 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
925 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
926 frame_offset.signal_connect('activate') {
927 if possible_actions[:can_multiple] && $selected_elements.length > 0
928 if values = ask_new_frame_offset(nil, '')
929 distribute_multiple_call.call(:frame_offset, values)
932 closures[:frame_offset].call
937 menu.append( Gtk::SeparatorMenuItem.new)
938 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
939 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
940 gammacorrect.signal_connect('activate') {
941 if possible_actions[:can_multiple] && $selected_elements.length > 0
942 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
943 distribute_multiple_call.call(:gammacorrect, values)
946 closures[:gammacorrect].call
949 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
950 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
951 whitebalance.signal_connect('activate') {
952 if possible_actions[:can_multiple] && $selected_elements.length > 0
953 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
954 distribute_multiple_call.call(:whitebalance, values)
957 closures[:whitebalance].call
960 if !possible_actions[:can_multiple] || $selected_elements.length == 0
961 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
962 _("Enhance constrast"))))
964 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
966 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
967 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
968 if type == 'image' && possible_actions[:can_panorama]
969 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
970 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
971 panorama.signal_connect('activate') {
972 if possible_actions[:can_multiple] && $selected_elements.length > 0
973 if values = ask_new_pano_amount(nil, '')
974 distribute_multiple_call.call(:pano, values)
977 distribute_multiple_call.call(:pano)
981 menu.append( Gtk::SeparatorMenuItem.new)
982 if optionals.include?('delete')
983 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
984 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
985 if !possible_actions[:can_multiple] || $selected_elements.length == 0
986 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
987 paste_item.signal_connect('activate') { closures[:paste].call }
988 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
989 clear_item.signal_connect('activate') { $cuts = [] }
991 paste_item.sensitive = clear_item.sensitive = false
994 menu.append( Gtk::SeparatorMenuItem.new)
996 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
997 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
998 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
999 editexternally.signal_connect('activate') {
1000 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1005 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1006 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1007 if optionals.include?('delete')
1008 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1009 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1012 menu.popup(nil, nil, event.button, event.time)
1015 def delete_current_subalbum
1017 sel = $albums_tv.selection.selected_rows
1018 $xmldir.elements.each { |e|
1019 if e.name == 'image' || e.name == 'video'
1020 e.add_attribute('deleted', 'true')
1023 #- branch if we have a non deleted subalbum
1024 if $xmldir.child_byname_notattr('dir', 'deleted')
1025 $xmldir.delete_attribute('thumbnails-caption')
1026 $xmldir.delete_attribute('thumbnails-captionfile')
1028 $xmldir.add_attribute('deleted', 'true')
1030 while moveup.parent.name == 'dir'
1031 moveup = moveup.parent
1032 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1033 moveup.add_attribute('deleted', 'true')
1040 save_changes('forced')
1041 populate_subalbums_treeview(false)
1042 $albums_tv.selection.select_path(sel[0])
1048 $current_path = nil #- prevent save_changes from being rerun again
1049 sel = $albums_tv.selection.selected_rows
1050 restore_one = proc { |xmldir|
1051 xmldir.elements.each { |e|
1052 if e.name == 'dir' && e.attributes['deleted']
1055 e.delete_attribute('deleted')
1058 restore_one.call($xmldir)
1059 populate_subalbums_treeview(false)
1060 $albums_tv.selection.select_path(sel[0])
1063 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1066 frame1 = Gtk::Frame.new
1067 fullpath = from_utf8("#{$current_path}/#{filename}")
1069 my_gen_real_thumbnail = proc {
1070 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1074 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1075 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1076 pack_start(img = Gtk::Image.new).
1077 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1078 px, mask = pxb.render_pixmap_and_mask
1079 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1080 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1082 frame1.add(img = Gtk::Image.new)
1085 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1086 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1087 my_gen_real_thumbnail.call
1089 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1092 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1094 tooltips = Gtk::Tooltips.new
1095 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1096 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1098 frame2, textview = create_editzone($autotable_sw, 1, img)
1099 textview.buffer.text = caption
1100 textview.set_justification(Gtk::Justification::CENTER)
1102 vbox = Gtk::VBox.new(false, 5)
1103 vbox.pack_start(evtbox, false, false)
1104 vbox.pack_start(frame2, false, false)
1105 autotable.append(vbox, filename)
1107 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1108 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1110 #- to be able to find widgets by name
1111 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1113 cleanup_all_thumbnails = proc {
1114 #- remove out of sync images
1115 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1116 for sizeobj in $images_size
1117 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
1123 cleanup_all_thumbnails.call
1124 my_gen_real_thumbnail.call
1127 rotate_and_cleanup = proc { |angle|
1128 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1129 cleanup_all_thumbnails.call
1132 move = proc { |direction|
1133 do_method = "move_#{direction}"
1134 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1136 done = autotable.method(do_method).call(vbox)
1137 textview.grab_focus #- because if moving, focus is stolen
1141 save_undo(_("move %s") % direction,
1143 autotable.method(undo_method).call(vbox)
1144 textview.grab_focus #- because if moving, focus is stolen
1145 autoscroll_if_needed($autotable_sw, img, textview)
1146 $notebook.set_page(1)
1148 autotable.method(do_method).call(vbox)
1149 textview.grab_focus #- because if moving, focus is stolen
1150 autoscroll_if_needed($autotable_sw, img, textview)
1151 $notebook.set_page(1)
1157 color_swap_and_cleanup = proc {
1158 perform_color_swap_and_cleanup = proc {
1159 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1160 my_gen_real_thumbnail.call
1163 cleanup_all_thumbnails.call
1164 perform_color_swap_and_cleanup.call
1166 save_undo(_("color swap"),
1168 perform_color_swap_and_cleanup.call
1170 autoscroll_if_needed($autotable_sw, img, textview)
1171 $notebook.set_page(1)
1173 perform_color_swap_and_cleanup.call
1175 autoscroll_if_needed($autotable_sw, img, textview)
1176 $notebook.set_page(1)
1181 change_frame_offset_and_cleanup_real = proc { |values|
1182 perform_change_frame_offset_and_cleanup = proc { |val|
1183 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
1184 my_gen_real_thumbnail.call
1186 perform_change_frame_offset_and_cleanup.call(values[:new])
1188 save_undo(_("specify frame offset"),
1190 perform_change_frame_offset_and_cleanup.call(values[:old])
1192 autoscroll_if_needed($autotable_sw, img, textview)
1193 $notebook.set_page(1)
1195 perform_change_frame_offset_and_cleanup.call(values[:new])
1197 autoscroll_if_needed($autotable_sw, img, textview)
1198 $notebook.set_page(1)
1203 change_frame_offset_and_cleanup = proc {
1204 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
1205 change_frame_offset_and_cleanup_real.call(values)
1209 change_pano_amount_and_cleanup_real = proc { |values|
1210 perform_change_pano_amount_and_cleanup = proc { |val|
1211 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1213 perform_change_pano_amount_and_cleanup.call(values[:new])
1215 save_undo(_("change panorama amount"),
1217 perform_change_pano_amount_and_cleanup.call(values[:old])
1219 autoscroll_if_needed($autotable_sw, img, textview)
1220 $notebook.set_page(1)
1222 perform_change_pano_amount_and_cleanup.call(values[:new])
1224 autoscroll_if_needed($autotable_sw, img, textview)
1225 $notebook.set_page(1)
1230 change_pano_amount_and_cleanup = proc {
1231 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1232 change_pano_amount_and_cleanup_real.call(values)
1236 whitebalance_and_cleanup_real = proc { |values|
1237 perform_change_whitebalance_and_cleanup = proc { |val|
1238 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1239 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1240 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1241 cleanup_all_thumbnails.call
1243 perform_change_whitebalance_and_cleanup.call(values[:new])
1245 save_undo(_("fix white balance"),
1247 perform_change_whitebalance_and_cleanup.call(values[:old])
1249 autoscroll_if_needed($autotable_sw, img, textview)
1250 $notebook.set_page(1)
1252 perform_change_whitebalance_and_cleanup.call(values[:new])
1254 autoscroll_if_needed($autotable_sw, img, textview)
1255 $notebook.set_page(1)
1260 whitebalance_and_cleanup = proc {
1261 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1262 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1263 whitebalance_and_cleanup_real.call(values)
1267 gammacorrect_and_cleanup_real = proc { |values|
1268 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1269 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1270 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1271 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1272 cleanup_all_thumbnails.call
1274 perform_change_gammacorrect_and_cleanup.call(values[:new])
1276 save_undo(_("gamma correction"),
1278 perform_change_gammacorrect_and_cleanup.call(values[:old])
1280 autoscroll_if_needed($autotable_sw, img, textview)
1281 $notebook.set_page(1)
1283 perform_change_gammacorrect_and_cleanup.call(values[:new])
1285 autoscroll_if_needed($autotable_sw, img, textview)
1286 $notebook.set_page(1)
1291 gammacorrect_and_cleanup = Proc.new {
1292 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1293 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1294 gammacorrect_and_cleanup_real.call(values)
1298 enhance_and_cleanup = proc {
1299 perform_enhance_and_cleanup = proc {
1300 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1301 my_gen_real_thumbnail.call
1304 cleanup_all_thumbnails.call
1305 perform_enhance_and_cleanup.call
1307 save_undo(_("enhance"),
1309 perform_enhance_and_cleanup.call
1311 autoscroll_if_needed($autotable_sw, img, textview)
1312 $notebook.set_page(1)
1314 perform_enhance_and_cleanup.call
1316 autoscroll_if_needed($autotable_sw, img, textview)
1317 $notebook.set_page(1)
1322 delete = proc { |isacut|
1323 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 })
1326 perform_delete = proc {
1327 after = autotable.get_next_widget(vbox)
1329 after = autotable.get_previous_widget(vbox)
1331 if $config['deleteondisk'] && !isacut
1332 msg 3, "scheduling for delete: #{fullpath}"
1333 $todelete << fullpath
1335 autotable.remove(vbox)
1337 $vbox2widgets[after][:textview].grab_focus
1338 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1342 previous_pos = autotable.get_current_number(vbox)
1346 delete_current_subalbum
1348 save_undo(_("delete"),
1350 autotable.reinsert(pos, vbox, filename)
1351 $notebook.set_page(1)
1352 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1354 msg 3, "removing deletion schedule of: #{fullpath}"
1355 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1358 $notebook.set_page(1)
1367 $cuts << { :vbox => vbox, :filename => filename }
1368 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1373 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1376 autotable.queue_draws << proc {
1377 $vbox2widgets[last[:vbox]][:textview].grab_focus
1378 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1380 save_undo(_("paste"),
1382 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1383 $notebook.set_page(1)
1386 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1388 $notebook.set_page(1)
1391 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1396 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1397 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1398 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1399 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1401 textview.signal_connect('key-press-event') { |w, event|
1404 x, y = autotable.get_current_pos(vbox)
1405 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1406 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1407 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1408 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1410 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1411 $vbox2widgets[widget_up][:textview].grab_focus
1418 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1420 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1421 $vbox2widgets[widget_down][:textview].grab_focus
1428 if event.keyval == Gdk::Keyval::GDK_Left
1431 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1438 rotate_and_cleanup.call(-90)
1441 if event.keyval == Gdk::Keyval::GDK_Right
1442 next_ = autotable.get_next_widget(vbox)
1443 if next_ && autotable.get_current_pos(next_)[0] > x
1445 $vbox2widgets[next_][:textview].grab_focus
1452 rotate_and_cleanup.call(90)
1455 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1458 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1459 view_element(filename, { :delete => delete })
1462 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1465 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1469 !propagate #- propagate if needed
1472 $ignore_next_release = false
1473 evtbox.signal_connect('button-press-event') { |w, event|
1474 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1475 if event.state & Gdk::Window::BUTTON3_MASK != 0
1476 #- gesture redo: hold right mouse button then click left mouse button
1477 $config['nogestures'] or perform_redo
1478 $ignore_next_release = true
1480 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1482 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1484 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1485 elsif $enhance.active?
1486 enhance_and_cleanup.call
1487 elsif $delete.active?
1491 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1494 $button1_pressed_autotable = true
1495 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1496 if event.state & Gdk::Window::BUTTON1_MASK != 0
1497 #- gesture undo: hold left mouse button then click right mouse button
1498 $config['nogestures'] or perform_undo
1499 $ignore_next_release = true
1501 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1502 view_element(filename, { :delete => delete })
1507 evtbox.signal_connect('button-release-event') { |w, event|
1508 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1509 if !$ignore_next_release
1510 x, y = autotable.get_current_pos(vbox)
1511 next_ = autotable.get_next_widget(vbox)
1512 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1513 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1514 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1515 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1516 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1517 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1518 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1520 $ignore_next_release = false
1521 $gesture_press = nil
1526 #- handle reordering with drag and drop
1527 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1528 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1529 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1530 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1533 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1535 #- mouse gesture first (dnd disables button-release-event)
1536 if $gesture_press && $gesture_press[:filename] == filename
1537 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1538 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1539 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1540 rotate_and_cleanup.call(angle)
1541 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1543 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1544 msg 3, "gesture delete: click-drag right button to the bottom"
1546 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1551 ctxt.targets.each { |target|
1552 if target.name == 'reorder-elements'
1553 move_dnd = proc { |from,to|
1556 autotable.move(from, to)
1557 save_undo(_("reorder"),
1560 autotable.move(to - 1, from)
1562 autotable.move(to, from + 1)
1564 $notebook.set_page(1)
1566 autotable.move(from, to)
1567 $notebook.set_page(1)
1572 if $multiple_dnd.size == 0
1573 move_dnd.call(selection_data.data.to_i,
1574 autotable.get_current_number(vbox))
1576 UndoHandler.begin_batch
1577 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1579 #- need to update current position between each call
1580 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1581 autotable.get_current_number(vbox))
1583 UndoHandler.end_batch
1594 def create_auto_table
1596 $autotable = Gtk::AutoTable.new(5)
1598 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1599 thumbnails_vb = Gtk::VBox.new(false, 5)
1601 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1602 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1603 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1604 thumbnails_vb.add($autotable)
1606 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1607 $autotable_sw.add_with_viewport(thumbnails_vb)
1609 #- follows stuff for handling multiple elements selection
1610 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1612 update_selected = proc {
1613 $autotable.current_order.each { |path|
1614 w = $name2widgets[path][:evtbox].window
1615 xm = w.position[0] + w.size[0]/2
1616 ym = w.position[1] + w.size[1]/2
1617 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1618 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1619 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1620 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1623 if $selected_elements[path] && ! $selected_elements[path][:keep]
1624 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))
1625 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1626 $selected_elements.delete(path)
1631 $autotable.signal_connect('realize') { |w,e|
1632 gc = Gdk::GC.new($autotable.window)
1633 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1634 gc.function = Gdk::GC::INVERT
1635 #- autoscroll handling for DND and multiple selections
1636 Gtk.timeout_add(100) {
1637 if ! $autotable.window.nil?
1638 w, x, y, mask = $autotable.window.pointer
1639 if mask & Gdk::Window::BUTTON1_MASK != 0
1640 if y < $autotable_sw.vadjustment.value
1642 $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]])
1644 if $button1_pressed_autotable || press_x
1645 scroll_upper($autotable_sw, y)
1648 w, pos_x, pos_y = $autotable.window.pointer
1649 $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 update_selected.call
1653 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
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]])
1657 if $button1_pressed_autotable || press_x
1658 scroll_lower($autotable_sw, y)
1661 w, pos_x, pos_y = $autotable.window.pointer
1662 $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 update_selected.call
1668 ! $autotable.window.nil?
1672 $autotable.signal_connect('button-press-event') { |w,e|
1674 if !$button1_pressed_autotable
1677 if e.state & Gdk::Window::SHIFT_MASK == 0
1678 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1679 $selected_elements = {}
1680 $statusbar.push(0, utf8(_("Nothing selected.")))
1682 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1684 set_mousecursor(Gdk::Cursor::TCROSS)
1688 $autotable.signal_connect('button-release-event') { |w,e|
1690 if $button1_pressed_autotable
1691 #- unselect all only now
1692 $multiple_dnd = $selected_elements.keys
1693 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1694 $selected_elements = {}
1695 $button1_pressed_autotable = false
1698 $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]])
1699 if $selected_elements.length > 0
1700 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1703 press_x = press_y = pos_x = pos_y = nil
1704 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1708 $autotable.signal_connect('motion-notify-event') { |w,e|
1711 $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]])
1715 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1716 update_selected.call
1722 def create_subalbums_page
1724 subalbums_hb = Gtk::HBox.new
1725 $subalbums_vb = Gtk::VBox.new(false, 5)
1726 subalbums_hb.pack_start($subalbums_vb, false, false)
1727 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1728 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1729 $subalbums_sw.add_with_viewport(subalbums_hb)
1732 def save_current_file
1738 ios = File.open($filename, "w")
1739 $xmldoc.write(ios, 0)
1741 rescue Iconv::IllegalSequence
1742 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1743 if ! ios.nil? && ! ios.closed?
1746 $xmldoc.xml_decl.encoding = 'UTF-8'
1747 ios = File.open($filename, "w")
1748 $xmldoc.write(ios, 0)
1759 def save_current_file_user
1760 save_tempfilename = $filename
1761 $filename = $orig_filename
1762 if ! save_current_file
1763 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1764 $filename = save_tempfilename
1768 $generated_outofline = false
1769 $filename = save_tempfilename
1771 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1772 $todelete.each { |f|
1773 system("rm -f #{f}")
1777 def mark_document_as_dirty
1778 $xmldoc.elements.each('//dir') { |elem|
1779 elem.delete_attribute('already-generated')
1783 #- ret: true => ok false => cancel
1784 def ask_save_modifications(msg1, msg2, *options)
1786 options = options.size > 0 ? options[0] : {}
1788 if options[:disallow_cancel]
1789 dialog = Gtk::Dialog.new(msg1,
1791 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1792 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1793 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1795 dialog = Gtk::Dialog.new(msg1,
1797 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1798 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1799 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1800 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1802 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1803 dialog.vbox.add(Gtk::Label.new(msg2))
1804 dialog.window_position = Gtk::Window::POS_CENTER
1807 dialog.run { |response|
1809 if response == Gtk::Dialog::RESPONSE_YES
1810 if ! save_current_file_user
1811 return ask_save_modifications(msg1, msg2, options)
1814 #- if we have generated an album but won't save modifications, we must remove
1815 #- already-generated markers in original file
1816 if $generated_outofline
1818 $xmldoc = REXML::Document.new File.new($orig_filename)
1819 mark_document_as_dirty
1820 ios = File.open($orig_filename, "w")
1821 $xmldoc.write(ios, 0)
1824 puts "exception: #{$!}"
1828 if response == Gtk::Dialog::RESPONSE_CANCEL
1831 $todelete = [] #- unconditionally clear the list of images/videos to delete
1837 def try_quit(*options)
1838 if ask_save_modifications(utf8(_("Save before quitting?")),
1839 utf8(_("Do you want to save your changes before quitting?")),
1845 def show_popup(parent, msg, *options)
1846 dialog = Gtk::Dialog.new
1847 if options[0] && options[0][:title]
1848 dialog.title = options[0][:title]
1850 dialog.title = utf8(_("Booh message"))
1852 lbl = Gtk::Label.new
1853 if options[0] && options[0][:nomarkup]
1858 if options[0] && options[0][:centered]
1859 lbl.set_justify(Gtk::Justification::CENTER)
1861 if options[0] && options[0][:selectable]
1862 lbl.selectable = true
1864 if options[0] && options[0][:topwidget]
1865 dialog.vbox.add(options[0][:topwidget])
1867 if options[0] && options[0][:scrolled]
1868 sw = Gtk::ScrolledWindow.new(nil, nil)
1869 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1870 sw.add_with_viewport(lbl)
1872 dialog.set_default_size(500, 600)
1874 dialog.vbox.add(lbl)
1875 dialog.set_default_size(200, 120)
1877 if options[0] && options[0][:okcancel]
1878 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1880 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1882 if options[0] && options[0][:pos_centered]
1883 dialog.window_position = Gtk::Window::POS_CENTER
1885 dialog.window_position = Gtk::Window::POS_MOUSE
1888 if options[0] && options[0][:linkurl]
1889 linkbut = Gtk::Button.new('')
1890 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1891 linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1892 linkbut.relief = Gtk::RELIEF_NONE
1893 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1894 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1895 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1900 if !options[0] || !options[0][:not_transient]
1901 dialog.transient_for = parent
1902 dialog.run { |response|
1904 if options[0] && options[0][:okcancel]
1905 return response == Gtk::Dialog::RESPONSE_OK
1909 dialog.signal_connect('response') { dialog.destroy }
1913 def backend_wait_message(parent, msg, infopipe_path, mode)
1915 w.set_transient_for(parent)
1918 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1919 vb.pack_start(Gtk::Label.new(msg), false, false)
1921 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1922 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1923 if mode != 'one dir scan'
1924 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1926 if mode == 'web-album'
1927 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1928 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1930 vb.pack_start(Gtk::HSeparator.new, false, false)
1932 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1933 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1934 vb.pack_end(bottom, false, false)
1936 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1937 refresh_thread = Thread.new {
1938 directories_counter = 0
1939 while line = infopipe.gets
1940 if line =~ /^directories: (\d+), sizes: (\d+)/
1941 directories = $1.to_f + 1
1943 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1944 elements = $3.to_f + 1
1945 if mode == 'web-album'
1949 gtk_thread_protect { pb1_1.fraction = 0 }
1950 if mode != 'one dir scan'
1951 newtext = utf8(full_src_dir_to_rel($1, $2))
1952 newtext = '/' if newtext == ''
1953 gtk_thread_protect { pb1_2.text = newtext }
1954 directories_counter += 1
1955 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1957 elsif line =~ /^processing element$/
1958 element_counter += 1
1959 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1960 elsif line =~ /^processing size$/
1961 element_counter += 1
1962 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1963 elsif line =~ /^finished processing sizes$/
1964 gtk_thread_protect { pb1_1.fraction = 1 }
1965 elsif line =~ /^creating index.html$/
1966 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1967 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1968 directories_counter = 0
1969 elsif line =~ /^index.html: (.+)\|(.+)/
1970 newtext = utf8(full_src_dir_to_rel($1, $2))
1971 newtext = '/' if newtext == ''
1972 gtk_thread_protect { pb2.text = newtext }
1973 directories_counter += 1
1974 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1975 elsif line =~ /^die: (.*)$/
1982 w.signal_connect('delete-event') { w.destroy }
1983 w.signal_connect('destroy') {
1984 Thread.kill(refresh_thread)
1985 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1988 system("rm -f #{infopipe_path}")
1991 w.window_position = Gtk::Window::POS_CENTER
1997 def call_backend(cmd, waitmsg, mode, params)
1998 pipe = Tempfile.new("boohpipe")
2000 system("mkfifo #{pipe.path}")
2001 cmd += " --info-pipe #{pipe.path}"
2002 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2007 id, exitstatus = Process.waitpid2(pid)
2008 gtk_thread_protect { w8.destroy }
2010 if params[:successmsg]
2011 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2013 if params[:closure_after]
2014 gtk_thread_protect(¶ms[:closure_after])
2016 elsif exitstatus == 15
2017 #- say nothing, user aborted
2019 gtk_thread_protect { show_popup($main_window,
2020 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2026 button.signal_connect('clicked') {
2027 Process.kill('SIGTERM', pid)
2031 def save_changes(*forced)
2032 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2036 $xmldir.delete_attribute('already-generated')
2038 propagate_children = proc { |xmldir|
2039 if xmldir.attributes['subdirs-caption']
2040 xmldir.delete_attribute('already-generated')
2042 xmldir.elements.each('dir') { |element|
2043 propagate_children.call(element)
2047 if $xmldir.child_byname_notattr('dir', 'deleted')
2048 new_title = $subalbums_title.buffer.text
2049 if new_title != $xmldir.attributes['subdirs-caption']
2050 parent = $xmldir.parent
2051 if parent.name == 'dir'
2052 parent.delete_attribute('already-generated')
2054 propagate_children.call($xmldir)
2056 $xmldir.add_attribute('subdirs-caption', new_title)
2057 $xmldir.elements.each('dir') { |element|
2058 if !element.attributes['deleted']
2059 path = element.attributes['path']
2060 newtext = $subalbums_edits[path][:editzone].buffer.text
2061 if element.attributes['subdirs-caption']
2062 if element.attributes['subdirs-caption'] != newtext
2063 propagate_children.call(element)
2065 element.add_attribute('subdirs-caption', newtext)
2066 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2068 if element.attributes['thumbnails-caption'] != newtext
2069 element.delete_attribute('already-generated')
2071 element.add_attribute('thumbnails-caption', newtext)
2072 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2078 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2079 if $xmldir.attributes['thumbnails-caption']
2080 path = $xmldir.attributes['path']
2081 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2083 elsif $xmldir.attributes['thumbnails-caption']
2084 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2087 if $xmldir.attributes['thumbnails-caption']
2088 if edit = $subalbums_edits[$xmldir.attributes['path']]
2089 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2093 #- remove and reinsert elements to reflect new ordering
2096 $xmldir.elements.each { |element|
2097 if element.name == 'image' || element.name == 'video'
2098 saves[element.attributes['filename']] = element.remove
2102 $autotable.current_order.each { |path|
2103 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2104 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2107 saves.each_key { |path|
2108 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2109 chld.add_attribute('deleted', 'true')
2113 def sort_by_exif_date
2117 $xmldir.elements.each { |element|
2118 if element.name == 'image' || element.name == 'video'
2119 current_order << element.attributes['filename']
2123 #- look for EXIF dates
2125 w.set_transient_for($main_window)
2127 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2128 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2129 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2130 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2131 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2132 vb.pack_end(bottom, false, false)
2134 w.signal_connect('delete-event') { w.destroy }
2135 w.window_position = Gtk::Window::POS_CENTER
2139 b.signal_connect('clicked') { aborted = true }
2142 current_order.each { |f|
2144 if entry2type(f) == 'image'
2146 pb.fraction = i.to_f / current_order.size
2147 Gtk.main_iteration while Gtk.events_pending?
2148 date_time = `identify -format "%[EXIF:DateTimeOriginal]" '#{from_utf8($current_path + "/" + f)}'`.chomp
2149 if $? == 0 && date_time != ''
2150 dates[f] = date_time
2163 $xmldir.elements.each { |element|
2164 if element.name == 'image' || element.name == 'video'
2165 saves[element.attributes['filename']] = element.remove
2169 #- find a good fallback for all entries without a date (still next to the item they were next to)
2170 neworder = dates.keys.sort { |a,b| dates[a] <=> dates[b] }
2171 for i in 0 .. current_order.size - 1
2172 if ! neworder.include?(current_order[i])
2174 while j > 0 && ! neworder.include?(current_order[j])
2177 neworder[(neworder.index(current_order[j]) || -1 ) + 1, 0] = current_order[i]
2181 $xmldir.add_element(saves[f].name, saves[f].attributes)
2184 #- let the auto-table reflect new ordering
2188 def remove_all_captions
2191 $autotable.current_order.each { |path|
2192 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2193 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2195 save_undo(_("remove all captions"),
2197 texts.each_key { |key|
2198 $name2widgets[key][:textview].buffer.text = texts[key]
2200 $notebook.set_page(1)
2202 texts.each_key { |key|
2203 $name2widgets[key][:textview].buffer.text = ''
2205 $notebook.set_page(1)
2211 $selected_elements.each_key { |path|
2212 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2218 $selected_elements = {}
2222 $undo_tb.sensitive = $undo_mb.sensitive = false
2223 $redo_tb.sensitive = $redo_mb.sensitive = false
2229 $subalbums_vb.children.each { |chld|
2230 $subalbums_vb.remove(chld)
2232 $subalbums = Gtk::Table.new(0, 0, true)
2233 current_y_sub_albums = 0
2235 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2236 $subalbums_edits = {}
2237 subalbums_counter = 0
2238 subalbums_edits_bypos = {}
2240 add_subalbum = proc { |xmldir, counter|
2241 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2242 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2243 if xmldir == $xmldir
2244 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2245 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2246 caption = xmldir.attributes['thumbnails-caption']
2247 infotype = 'thumbnails'
2249 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2250 captionfile, caption = find_subalbum_caption_info(xmldir)
2251 infotype = find_subalbum_info_type(xmldir)
2253 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2254 hbox = Gtk::HBox.new
2255 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2257 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2260 my_gen_real_thumbnail = proc {
2261 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2264 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2265 f.add(img = Gtk::Image.new)
2266 my_gen_real_thumbnail.call
2268 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2270 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2271 $subalbums.attach(hbox,
2272 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2274 frame, textview = create_editzone($subalbums_sw, 0, img)
2275 textview.buffer.text = caption
2276 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2277 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2279 change_image = proc {
2280 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2282 Gtk::FileChooser::ACTION_OPEN,
2284 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2285 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2286 fc.transient_for = $main_window
2287 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))
2288 f.add(preview_img = Gtk::Image.new)
2290 fc.signal_connect('update-preview') { |w|
2292 if fc.preview_filename
2293 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2294 fc.preview_widget_active = true
2296 rescue Gdk::PixbufError
2297 fc.preview_widget_active = false
2300 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2302 old_file = captionfile
2303 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2304 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2305 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2306 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2308 new_file = fc.filename
2309 msg 3, "new captionfile is: #{fc.filename}"
2310 perform_changefile = proc {
2311 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2312 $modified_pixbufs.delete(thumbnail_file)
2313 xmldir.delete_attribute("#{infotype}-rotate")
2314 xmldir.delete_attribute("#{infotype}-color-swap")
2315 xmldir.delete_attribute("#{infotype}-enhance")
2316 xmldir.delete_attribute("#{infotype}-frame-offset")
2317 my_gen_real_thumbnail.call
2319 perform_changefile.call
2321 save_undo(_("change caption file for sub-album"),
2323 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2324 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2325 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2326 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2327 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2328 my_gen_real_thumbnail.call
2329 $notebook.set_page(0)
2331 perform_changefile.call
2332 $notebook.set_page(0)
2340 system("rm -f '#{thumbnail_file}'")
2341 my_gen_real_thumbnail.call
2344 rotate_and_cleanup = proc { |angle|
2345 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2346 system("rm -f '#{thumbnail_file}'")
2349 move = proc { |direction|
2352 save_changes('forced')
2353 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2354 if direction == 'up'
2355 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2356 subalbums_edits_bypos[oldpos - 1][:position] += 1
2358 if direction == 'down'
2359 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2360 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2362 if direction == 'top'
2363 for i in 1 .. oldpos - 1
2364 subalbums_edits_bypos[i][:position] += 1
2366 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2368 if direction == 'bottom'
2369 for i in oldpos + 1 .. subalbums_counter
2370 subalbums_edits_bypos[i][:position] -= 1
2372 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2376 $xmldir.elements.each('dir') { |element|
2377 if (!element.attributes['deleted'])
2378 elems << [ element.attributes['path'], element.remove ]
2381 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2382 each { |e| $xmldir.add_element(e[1]) }
2383 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2384 $xmldir.elements.each('descendant::dir') { |elem|
2385 elem.delete_attribute('already-generated')
2388 sel = $albums_tv.selection.selected_rows
2390 populate_subalbums_treeview(false)
2391 $albums_tv.selection.select_path(sel[0])
2394 color_swap_and_cleanup = proc {
2395 perform_color_swap_and_cleanup = proc {
2396 color_swap(xmldir, "#{infotype}-")
2397 my_gen_real_thumbnail.call
2399 perform_color_swap_and_cleanup.call
2401 save_undo(_("color swap"),
2403 perform_color_swap_and_cleanup.call
2404 $notebook.set_page(0)
2406 perform_color_swap_and_cleanup.call
2407 $notebook.set_page(0)
2412 change_frame_offset_and_cleanup = proc {
2413 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2414 perform_change_frame_offset_and_cleanup = proc { |val|
2415 change_frame_offset(xmldir, "#{infotype}-", val)
2416 my_gen_real_thumbnail.call
2418 perform_change_frame_offset_and_cleanup.call(values[:new])
2420 save_undo(_("specify frame offset"),
2422 perform_change_frame_offset_and_cleanup.call(values[:old])
2423 $notebook.set_page(0)
2425 perform_change_frame_offset_and_cleanup.call(values[:new])
2426 $notebook.set_page(0)
2432 whitebalance_and_cleanup = proc {
2433 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2434 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2435 perform_change_whitebalance_and_cleanup = proc { |val|
2436 change_whitebalance(xmldir, "#{infotype}-", val)
2437 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2438 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2439 system("rm -f '#{thumbnail_file}'")
2441 perform_change_whitebalance_and_cleanup.call(values[:new])
2443 save_undo(_("fix white balance"),
2445 perform_change_whitebalance_and_cleanup.call(values[:old])
2446 $notebook.set_page(0)
2448 perform_change_whitebalance_and_cleanup.call(values[:new])
2449 $notebook.set_page(0)
2455 gammacorrect_and_cleanup = proc {
2456 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2457 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2458 perform_change_gammacorrect_and_cleanup = proc { |val|
2459 change_gammacorrect(xmldir, "#{infotype}-", val)
2460 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2461 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2462 system("rm -f '#{thumbnail_file}'")
2464 perform_change_gammacorrect_and_cleanup.call(values[:new])
2466 save_undo(_("gamma correction"),
2468 perform_change_gammacorrect_and_cleanup.call(values[:old])
2469 $notebook.set_page(0)
2471 perform_change_gammacorrect_and_cleanup.call(values[:new])
2472 $notebook.set_page(0)
2478 enhance_and_cleanup = proc {
2479 perform_enhance_and_cleanup = proc {
2480 enhance(xmldir, "#{infotype}-")
2481 my_gen_real_thumbnail.call
2484 perform_enhance_and_cleanup.call
2486 save_undo(_("enhance"),
2488 perform_enhance_and_cleanup.call
2489 $notebook.set_page(0)
2491 perform_enhance_and_cleanup.call
2492 $notebook.set_page(0)
2497 evtbox.signal_connect('button-press-event') { |w, event|
2498 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2500 rotate_and_cleanup.call(90)
2502 rotate_and_cleanup.call(-90)
2503 elsif $enhance.active?
2504 enhance_and_cleanup.call
2507 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2508 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2509 { :forbid_left => true, :forbid_right => true,
2510 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2511 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2512 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2513 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2514 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2516 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2521 evtbox.signal_connect('button-press-event') { |w, event|
2522 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2526 evtbox.signal_connect('button-release-event') { |w, event|
2527 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2528 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2529 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2530 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2531 msg 3, "gesture rotate: #{angle}"
2532 rotate_and_cleanup.call(angle)
2535 $gesture_press = nil
2538 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2539 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2540 current_y_sub_albums += 1
2543 if $xmldir.child_byname_notattr('dir', 'deleted')
2545 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2546 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2547 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2548 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2549 #- this album image/caption
2550 if $xmldir.attributes['thumbnails-caption']
2551 add_subalbum.call($xmldir, 0)
2554 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2555 $xmldir.elements.each { |element|
2556 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2557 #- element (image or video) of this album
2558 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2559 msg 3, "dest_img: #{dest_img}"
2560 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2561 total[element.name] += 1
2563 if element.name == 'dir' && !element.attributes['deleted']
2564 #- sub-album image/caption
2565 add_subalbum.call(element, subalbums_counter += 1)
2566 total[element.name] += 1
2569 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2570 total['image'], total['video'], total['dir'] ]))
2571 $subalbums_vb.add($subalbums)
2572 $subalbums_vb.show_all
2574 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2575 $notebook.get_tab_label($autotable_sw).sensitive = false
2576 $notebook.set_page(0)
2577 $thumbnails_title.buffer.text = ''
2579 $notebook.get_tab_label($autotable_sw).sensitive = true
2580 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2583 if !$xmldir.child_byname_notattr('dir', 'deleted')
2584 $notebook.get_tab_label($subalbums_sw).sensitive = false
2585 $notebook.set_page(1)
2587 $notebook.get_tab_label($subalbums_sw).sensitive = true
2591 def pixbuf_or_nil(filename)
2593 return Gdk::Pixbuf.new(filename)
2599 def theme_choose(current)
2600 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2602 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2603 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2604 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2606 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2607 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2608 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2609 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2610 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2611 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2612 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2613 treeview.signal_connect('button-press-event') { |w, event|
2614 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2615 dialog.response(Gtk::Dialog::RESPONSE_OK)
2619 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2621 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2624 iter[0] = File.basename(dir)
2625 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2626 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2627 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2628 if File.basename(dir) == current
2629 treeview.selection.select_iter(iter)
2633 dialog.set_default_size(700, 400)
2634 dialog.vbox.show_all
2635 dialog.run { |response|
2636 iter = treeview.selection.selected
2638 if response == Gtk::Dialog::RESPONSE_OK && iter
2639 return model.get_value(iter, 0)
2645 def show_password_protections
2646 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2647 child_iter = $albums_iters[xmldir.attributes['path']]
2648 if xmldir.attributes['password-protect']
2649 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2650 already_protected = true
2651 elsif already_protected
2652 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2654 pix = pix.saturate_and_pixelate(1, true)
2660 xmldir.elements.each('dir') { |elem|
2661 if !elem.attributes['deleted']
2662 examine_dir_elem.call(child_iter, elem, already_protected)
2666 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2669 def populate_subalbums_treeview(select_first)
2673 $subalbums_vb.children.each { |chld|
2674 $subalbums_vb.remove(chld)
2677 source = $xmldoc.root.attributes['source']
2678 msg 3, "source: #{source}"
2680 xmldir = $xmldoc.elements['//dir']
2681 if !xmldir || xmldir.attributes['path'] != source
2682 msg 1, _("Corrupted booh file...")
2686 append_dir_elem = proc { |parent_iter, xmldir|
2687 child_iter = $albums_ts.append(parent_iter)
2688 child_iter[0] = File.basename(xmldir.attributes['path'])
2689 child_iter[1] = xmldir.attributes['path']
2690 $albums_iters[xmldir.attributes['path']] = child_iter
2691 msg 3, "puttin location: #{xmldir.attributes['path']}"
2692 xmldir.elements.each('dir') { |elem|
2693 if !elem.attributes['deleted']
2694 append_dir_elem.call(child_iter, elem)
2698 append_dir_elem.call(nil, xmldir)
2699 show_password_protections
2701 $albums_tv.expand_all
2703 $albums_tv.selection.select_iter($albums_ts.iter_first)
2707 def select_current_theme
2708 select_theme($xmldoc.root.attributes['theme'],
2709 $xmldoc.root.attributes['limit-sizes'],
2710 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2711 $xmldoc.root.attributes['thumbnails-per-row'])
2714 def open_file(filename)
2718 $current_path = nil #- invalidate
2719 $modified_pixbufs = {}
2722 $subalbums_vb.children.each { |chld|
2723 $subalbums_vb.remove(chld)
2726 if !File.exists?(filename)
2727 return utf8(_("File not found."))
2731 $xmldoc = REXML::Document.new File.new(filename)
2736 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2737 if entry2type(filename).nil?
2738 return utf8(_("Not a booh file!"))
2740 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."))
2744 if !source = $xmldoc.root.attributes['source']
2745 return utf8(_("Corrupted booh file..."))
2748 if !dest = $xmldoc.root.attributes['destination']
2749 return utf8(_("Corrupted booh file..."))
2752 if !theme = $xmldoc.root.attributes['theme']
2753 return utf8(_("Corrupted booh file..."))
2756 if $xmldoc.root.attributes['version'] < '0.8.6'
2757 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2758 mark_document_as_dirty
2759 if $xmldoc.root.attributes['version'] < '0.8.4'
2760 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2761 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2762 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2763 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2764 if old_dest_dir != new_dest_dir
2765 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2767 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2768 xmldir.elements.each { |element|
2769 if %w(image video).include?(element.name) && !element.attributes['deleted']
2770 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2771 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2772 Dir[old_name + '*'].each { |file|
2773 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2774 file != new_file and sys("mv '#{file}' '#{new_file}'")
2777 if element.name == 'dir' && !element.attributes['deleted']
2778 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2779 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2780 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2784 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2788 $xmldoc.root.add_attribute('version', $VERSION)
2791 select_current_theme
2793 $filename = filename
2794 $default_size['thumbnails'] =~ /(.*)x(.*)/
2795 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2796 $albums_thumbnail_size =~ /(.*)x(.*)/
2797 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2799 populate_subalbums_treeview(true)
2801 $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
2805 def open_file_user(filename)
2806 result = open_file(filename)
2808 $config['last-opens'] ||= []
2809 if $config['last-opens'][-1] != utf8(filename)
2810 $config['last-opens'] << utf8(filename)
2812 $orig_filename = $filename
2813 tmp = Tempfile.new("boohtemp")
2816 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2818 $tempfiles << $filename << "#{$filename}.backup"
2820 $orig_filename = nil
2826 if !ask_save_modifications(utf8(_("Save this album?")),
2827 utf8(_("Do you want to save the changes to this album?")),
2828 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2831 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2833 Gtk::FileChooser::ACTION_OPEN,
2835 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2836 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2837 fc.set_current_folder(File.expand_path("~/.booh"))
2838 fc.transient_for = $main_window
2841 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2842 push_mousecursor_wait(fc)
2843 msg = open_file_user(fc.filename)
2859 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2864 def additional_booh_options
2867 options += "--mproc #{$config['mproc'].to_i} "
2869 options += "--comments-format '#{$config['comments-format']}'"
2874 if !ask_save_modifications(utf8(_("Save this album?")),
2875 utf8(_("Do you want to save the changes to this album?")),
2876 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2879 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2881 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2882 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2883 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2885 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2886 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2887 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2888 tbl.attach(src = Gtk::Entry.new,
2889 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2890 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2891 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2892 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2893 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2894 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2895 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2896 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2897 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2898 tbl.attach(dest = Gtk::Entry.new,
2899 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2900 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2901 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2902 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2903 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2904 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2905 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2906 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2907 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2909 tooltips = Gtk::Tooltips.new
2910 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2911 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2912 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2913 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2914 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2915 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2916 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)
2917 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2918 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2919 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
2920 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
2921 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)
2922 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2923 pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
2924 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)
2926 src_nb_calculated_for = ''
2928 process_src_nb = proc {
2929 if src.text != src_nb_calculated_for
2930 src_nb_calculated_for = src.text
2932 Thread.kill(src_nb_thread)
2935 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
2936 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
2938 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2939 if File.readable?(from_utf8_safe(src_nb_calculated_for))
2940 src_nb_thread = Thread.new {
2941 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2942 total = { 'image' => 0, 'video' => 0, nil => 0 }
2943 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2944 if File.basename(dir) =~ /^\./
2948 Dir.entries(dir.chomp).each { |file|
2949 total[entry2type(file)] += 1
2951 rescue Errno::EACCES, Errno::ENOENT
2955 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2959 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2962 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2968 timeout_src_nb = Gtk.timeout_add(100) {
2972 src_browse.signal_connect('clicked') {
2973 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2975 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2977 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2978 fc.transient_for = $main_window
2979 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2980 src.text = utf8(fc.filename)
2982 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2987 dest_browse.signal_connect('clicked') {
2988 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2990 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2992 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2993 fc.transient_for = $main_window
2994 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2995 dest.text = utf8(fc.filename)
3000 conf_browse.signal_connect('clicked') {
3001 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3003 Gtk::FileChooser::ACTION_SAVE,
3005 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3006 fc.transient_for = $main_window
3007 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3008 fc.set_current_folder(File.expand_path("~/.booh"))
3009 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3010 conf.text = utf8(fc.filename)
3017 recreate_theme_config = proc {
3018 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3020 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3021 $images_size.each { |s|
3022 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3026 tooltips.set_tip(cb, utf8(s['description']), nil)
3027 theme_sizes << { :widget => cb, :value => s['name'] }
3029 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3030 tooltips = Gtk::Tooltips.new
3031 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3032 theme_sizes << { :widget => cb, :value => 'original' }
3035 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3038 $allowed_N_values.each { |n|
3040 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3042 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3044 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3048 nperrows << { :widget => rb, :value => n }
3050 nperrowradios.show_all
3052 recreate_theme_config.call
3054 theme_button.signal_connect('clicked') {
3055 if newtheme = theme_choose(theme_button.label)
3056 theme_button.label = newtheme
3057 recreate_theme_config.call
3061 dialog.vbox.add(frame1)
3062 dialog.vbox.add(frame2)
3063 dialog.window_position = Gtk::Window::POS_MOUSE
3069 dialog.run { |response|
3070 if response == Gtk::Dialog::RESPONSE_OK
3071 srcdir = from_utf8_safe(src.text)
3072 destdir = from_utf8_safe(dest.text)
3073 confpath = from_utf8_safe(conf.text)
3074 if src.text != '' && srcdir == ''
3075 show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3077 elsif !File.directory?(srcdir)
3078 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3080 elsif dest.text != '' && destdir == ''
3081 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3083 elsif destdir != make_dest_filename(destdir)
3084 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3086 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3087 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
3089 elsif File.exists?(destdir) && !File.directory?(destdir)
3090 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3092 elsif conf.text == ''
3093 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3095 elsif conf.text != '' && confpath == ''
3096 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3098 elsif File.directory?(confpath)
3099 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3101 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3102 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3104 system("mkdir '#{destdir}'")
3105 if !File.directory?(destdir)
3106 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3118 srcdir = from_utf8(src.text)
3119 destdir = from_utf8(dest.text)
3120 configskel = File.expand_path(from_utf8(conf.text))
3121 theme = theme_button.label
3122 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3123 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3124 opt432 = optimize432.active?
3125 madewith = madewithentry.text
3126 indexlink = indexlinkentry.text
3129 Thread.kill(src_nb_thread)
3130 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3133 Gtk.timeout_remove(timeout_src_nb)
3136 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3137 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3138 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3139 utf8(_("Please wait while scanning source directory...")),
3141 { :closure_after => proc { open_file_user(configskel) } })
3146 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3148 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3149 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3150 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3152 source = $xmldoc.root.attributes['source']
3153 dest = $xmldoc.root.attributes['destination']
3154 theme = $xmldoc.root.attributes['theme']
3155 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3156 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3157 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3159 limit_sizes = limit_sizes.split(/,/)
3161 madewith = $xmldoc.root.attributes['made-with']
3162 indexlink = $xmldoc.root.attributes['index-link']
3164 tooltips = Gtk::Tooltips.new
3165 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3166 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3167 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3168 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3169 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3170 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3171 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3172 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3173 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3174 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3175 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3176 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3177 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3179 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3180 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3181 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3182 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3183 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3184 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3185 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)
3186 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3187 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3189 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3190 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3192 indexlinkentry.text = indexlink
3194 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)
3195 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3196 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3198 madewithentry.text = madewith
3200 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)
3204 recreate_theme_config = proc {
3205 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3207 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3209 $images_size.each { |s|
3210 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3212 if limit_sizes.include?(s['name'])
3220 tooltips.set_tip(cb, utf8(s['description']), nil)
3221 theme_sizes << { :widget => cb, :value => s['name'] }
3223 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3224 tooltips = Gtk::Tooltips.new
3225 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3226 if limit_sizes && limit_sizes.include?('original')
3229 theme_sizes << { :widget => cb, :value => 'original' }
3232 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3235 $allowed_N_values.each { |n|
3237 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3239 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3241 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3242 nperrowradios.add(Gtk::Label.new(' '))
3243 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3246 nperrows << { :widget => rb, :value => n.to_s }
3248 nperrowradios.show_all
3250 recreate_theme_config.call
3252 theme_button.signal_connect('clicked') {
3253 if newtheme = theme_choose(theme_button.label)
3256 theme_button.label = newtheme
3257 recreate_theme_config.call
3261 dialog.vbox.add(frame1)
3262 dialog.vbox.add(frame2)
3263 dialog.window_position = Gtk::Window::POS_MOUSE
3269 dialog.run { |response|
3270 if response == Gtk::Dialog::RESPONSE_OK
3271 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3272 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3281 save_theme = theme_button.label
3282 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3283 save_opt432 = optimize432.active?
3284 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3285 save_madewith = madewithentry.text
3286 save_indexlink = indexlinkentry.text
3289 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith || save_indexlink != indexlinkentry)
3290 mark_document_as_dirty
3292 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3293 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3294 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3295 utf8(_("Please wait while scanning source directory...")),
3297 { :closure_after => proc {
3298 open_file($filename)
3302 #- select_theme merges global variables, need to return to current choices
3303 select_current_theme
3310 sel = $albums_tv.selection.selected_rows
3312 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3313 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3314 utf8(_("Please wait while scanning source directory...")),
3316 { :closure_after => proc {
3317 open_file($filename)
3318 $albums_tv.selection.select_path(sel[0])
3326 sel = $albums_tv.selection.selected_rows
3328 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3329 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3330 utf8(_("Please wait while scanning source directory...")),
3332 { :closure_after => proc {
3333 open_file($filename)
3334 $albums_tv.selection.select_path(sel[0])
3342 theme = $xmldoc.root.attributes['theme']
3343 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3345 limit_sizes = "--sizes #{limit_sizes}"
3347 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3348 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3349 utf8(_("Please wait while scanning source directory...")),
3351 { :closure_after => proc {
3352 open_file($filename)
3358 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3360 Gtk::FileChooser::ACTION_SAVE,
3362 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3363 fc.transient_for = $main_window
3364 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3365 fc.set_current_folder(File.expand_path("~/.booh"))
3366 fc.filename = $orig_filename
3367 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3368 $orig_filename = fc.filename
3369 if ! save_current_file_user
3373 $config['last-opens'] ||= []
3374 $config['last-opens'] << $orig_filename
3380 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3382 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3383 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3384 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3386 dialog.vbox.add(notebook = Gtk::Notebook.new)
3387 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3388 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3389 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3390 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)),
3391 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3392 tooltips = Gtk::Tooltips.new
3393 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3394 for example: /usr/bin/mplayer %f")), nil)
3395 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3396 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3397 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3398 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3399 tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3400 for example: /usr/bin/gimp-remote %f")), nil)
3401 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3402 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3403 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3404 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3405 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3406 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3407 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3408 0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3409 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)),
3410 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3411 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)
3412 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3413 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3414 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)
3415 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3416 0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3417 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)
3419 smp_check.signal_connect('toggled') {
3420 if smp_check.active?
3421 smp_hbox.sensitive = true
3423 smp_hbox.sensitive = false
3427 smp_check.active = true
3428 smp_spin.value = $config['mproc'].to_i
3430 nogestures_check.active = $config['nogestures']
3431 deleteondisk_check.active = $config['deleteondisk']
3433 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3434 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3435 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3436 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3437 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3438 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3439 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3440 tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3441 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3442 tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3443 2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3444 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)
3445 commentsformat_help.signal_connect('clicked') {
3446 show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3447 hence you should look at ImageMagick/identify documentation for the most
3448 accurate and up-to-date documentation. Last time I checked, documentation
3451 Print information about the image in a format of your choosing. You can
3452 include the image filename, type, width, height, Exif data, or other image
3453 attributes by embedding special format characters:
3456 %P page width and height
3460 %e filename extension
3465 %k number of unique colors
3472 %r image class and colorspace
3475 %u unique temporary filename