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 cleanup_all_thumbnails.call
1129 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
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 cleanup_all_thumbnails.call
1160 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1161 my_gen_real_thumbnail.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 cleanup_all_thumbnails.call
1184 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
1185 my_gen_real_thumbnail.call
1187 perform_change_frame_offset_and_cleanup.call(values[:new])
1189 save_undo(_("specify frame offset"),
1191 perform_change_frame_offset_and_cleanup.call(values[:old])
1193 autoscroll_if_needed($autotable_sw, img, textview)
1194 $notebook.set_page(1)
1196 perform_change_frame_offset_and_cleanup.call(values[:new])
1198 autoscroll_if_needed($autotable_sw, img, textview)
1199 $notebook.set_page(1)
1204 change_frame_offset_and_cleanup = proc {
1205 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
1206 change_frame_offset_and_cleanup_real.call(values)
1210 change_pano_amount_and_cleanup_real = proc { |values|
1211 perform_change_pano_amount_and_cleanup = proc { |val|
1212 cleanup_all_thumbnails.call
1213 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1215 perform_change_pano_amount_and_cleanup.call(values[:new])
1217 save_undo(_("change panorama amount"),
1219 perform_change_pano_amount_and_cleanup.call(values[:old])
1221 autoscroll_if_needed($autotable_sw, img, textview)
1222 $notebook.set_page(1)
1224 perform_change_pano_amount_and_cleanup.call(values[:new])
1226 autoscroll_if_needed($autotable_sw, img, textview)
1227 $notebook.set_page(1)
1232 change_pano_amount_and_cleanup = proc {
1233 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1234 change_pano_amount_and_cleanup_real.call(values)
1238 whitebalance_and_cleanup_real = proc { |values|
1239 perform_change_whitebalance_and_cleanup = proc { |val|
1240 cleanup_all_thumbnails.call
1241 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1242 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1243 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1245 perform_change_whitebalance_and_cleanup.call(values[:new])
1247 save_undo(_("fix white balance"),
1249 perform_change_whitebalance_and_cleanup.call(values[:old])
1251 autoscroll_if_needed($autotable_sw, img, textview)
1252 $notebook.set_page(1)
1254 perform_change_whitebalance_and_cleanup.call(values[:new])
1256 autoscroll_if_needed($autotable_sw, img, textview)
1257 $notebook.set_page(1)
1262 whitebalance_and_cleanup = proc {
1263 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1264 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1265 whitebalance_and_cleanup_real.call(values)
1269 gammacorrect_and_cleanup_real = proc { |values|
1270 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1271 cleanup_all_thumbnails.call
1272 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1273 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1274 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1276 perform_change_gammacorrect_and_cleanup.call(values[:new])
1278 save_undo(_("gamma correction"),
1280 perform_change_gammacorrect_and_cleanup.call(values[:old])
1282 autoscroll_if_needed($autotable_sw, img, textview)
1283 $notebook.set_page(1)
1285 perform_change_gammacorrect_and_cleanup.call(values[:new])
1287 autoscroll_if_needed($autotable_sw, img, textview)
1288 $notebook.set_page(1)
1293 gammacorrect_and_cleanup = Proc.new {
1294 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1295 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1296 gammacorrect_and_cleanup_real.call(values)
1300 enhance_and_cleanup = proc {
1301 perform_enhance_and_cleanup = proc {
1302 cleanup_all_thumbnails.call
1303 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1304 my_gen_real_thumbnail.call
1307 cleanup_all_thumbnails.call
1308 perform_enhance_and_cleanup.call
1310 save_undo(_("enhance"),
1312 perform_enhance_and_cleanup.call
1314 autoscroll_if_needed($autotable_sw, img, textview)
1315 $notebook.set_page(1)
1317 perform_enhance_and_cleanup.call
1319 autoscroll_if_needed($autotable_sw, img, textview)
1320 $notebook.set_page(1)
1325 delete = proc { |isacut|
1326 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 })
1329 perform_delete = proc {
1330 after = autotable.get_next_widget(vbox)
1332 after = autotable.get_previous_widget(vbox)
1334 if $config['deleteondisk'] && !isacut
1335 msg 3, "scheduling for delete: #{fullpath}"
1336 $todelete << fullpath
1338 autotable.remove(vbox)
1340 $vbox2widgets[after][:textview].grab_focus
1341 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1345 previous_pos = autotable.get_current_number(vbox)
1349 delete_current_subalbum
1351 save_undo(_("delete"),
1353 autotable.reinsert(pos, vbox, filename)
1354 $notebook.set_page(1)
1355 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1357 msg 3, "removing deletion schedule of: #{fullpath}"
1358 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1361 $notebook.set_page(1)
1370 $cuts << { :vbox => vbox, :filename => filename }
1371 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1376 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1379 autotable.queue_draws << proc {
1380 $vbox2widgets[last[:vbox]][:textview].grab_focus
1381 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1383 save_undo(_("paste"),
1385 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1386 $notebook.set_page(1)
1389 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1391 $notebook.set_page(1)
1394 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1399 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1400 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1401 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1402 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1404 textview.signal_connect('key-press-event') { |w, event|
1407 x, y = autotable.get_current_pos(vbox)
1408 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1409 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1410 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1411 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1413 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1414 $vbox2widgets[widget_up][:textview].grab_focus
1421 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1423 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1424 $vbox2widgets[widget_down][:textview].grab_focus
1431 if event.keyval == Gdk::Keyval::GDK_Left
1434 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1441 rotate_and_cleanup.call(-90)
1444 if event.keyval == Gdk::Keyval::GDK_Right
1445 next_ = autotable.get_next_widget(vbox)
1446 if next_ && autotable.get_current_pos(next_)[0] > x
1448 $vbox2widgets[next_][:textview].grab_focus
1455 rotate_and_cleanup.call(90)
1458 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1461 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1462 view_element(filename, { :delete => delete })
1465 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1468 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1472 !propagate #- propagate if needed
1475 $ignore_next_release = false
1476 evtbox.signal_connect('button-press-event') { |w, event|
1477 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1478 if event.state & Gdk::Window::BUTTON3_MASK != 0
1479 #- gesture redo: hold right mouse button then click left mouse button
1480 $config['nogestures'] or perform_redo
1481 $ignore_next_release = true
1483 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1485 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1487 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1488 elsif $enhance.active?
1489 enhance_and_cleanup.call
1490 elsif $delete.active?
1494 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1497 $button1_pressed_autotable = true
1498 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1499 if event.state & Gdk::Window::BUTTON1_MASK != 0
1500 #- gesture undo: hold left mouse button then click right mouse button
1501 $config['nogestures'] or perform_undo
1502 $ignore_next_release = true
1504 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1505 view_element(filename, { :delete => delete })
1510 evtbox.signal_connect('button-release-event') { |w, event|
1511 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1512 if !$ignore_next_release
1513 x, y = autotable.get_current_pos(vbox)
1514 next_ = autotable.get_next_widget(vbox)
1515 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1516 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1517 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1518 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1519 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1520 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1521 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1523 $ignore_next_release = false
1524 $gesture_press = nil
1529 #- handle reordering with drag and drop
1530 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1531 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1532 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1533 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1536 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1538 #- mouse gesture first (dnd disables button-release-event)
1539 if $gesture_press && $gesture_press[:filename] == filename
1540 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1541 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1542 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1543 rotate_and_cleanup.call(angle)
1544 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1546 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1547 msg 3, "gesture delete: click-drag right button to the bottom"
1549 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1554 ctxt.targets.each { |target|
1555 if target.name == 'reorder-elements'
1556 move_dnd = proc { |from,to|
1559 autotable.move(from, to)
1560 save_undo(_("reorder"),
1563 autotable.move(to - 1, from)
1565 autotable.move(to, from + 1)
1567 $notebook.set_page(1)
1569 autotable.move(from, to)
1570 $notebook.set_page(1)
1575 if $multiple_dnd.size == 0
1576 move_dnd.call(selection_data.data.to_i,
1577 autotable.get_current_number(vbox))
1579 UndoHandler.begin_batch
1580 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1582 #- need to update current position between each call
1583 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1584 autotable.get_current_number(vbox))
1586 UndoHandler.end_batch
1597 def create_auto_table
1599 $autotable = Gtk::AutoTable.new(5)
1601 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1602 thumbnails_vb = Gtk::VBox.new(false, 5)
1604 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1605 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1606 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1607 thumbnails_vb.add($autotable)
1609 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1610 $autotable_sw.add_with_viewport(thumbnails_vb)
1612 #- follows stuff for handling multiple elements selection
1613 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1615 update_selected = proc {
1616 $autotable.current_order.each { |path|
1617 w = $name2widgets[path][:evtbox].window
1618 xm = w.position[0] + w.size[0]/2
1619 ym = w.position[1] + w.size[1]/2
1620 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1621 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1622 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1623 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1626 if $selected_elements[path] && ! $selected_elements[path][:keep]
1627 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))
1628 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1629 $selected_elements.delete(path)
1634 $autotable.signal_connect('realize') { |w,e|
1635 gc = Gdk::GC.new($autotable.window)
1636 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1637 gc.function = Gdk::GC::INVERT
1638 #- autoscroll handling for DND and multiple selections
1639 Gtk.timeout_add(100) {
1640 if ! $autotable.window.nil?
1641 w, x, y, mask = $autotable.window.pointer
1642 if mask & Gdk::Window::BUTTON1_MASK != 0
1643 if y < $autotable_sw.vadjustment.value
1645 $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]])
1647 if $button1_pressed_autotable || press_x
1648 scroll_upper($autotable_sw, y)
1651 w, pos_x, pos_y = $autotable.window.pointer
1652 $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]])
1653 update_selected.call
1656 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1658 $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]])
1660 if $button1_pressed_autotable || press_x
1661 scroll_lower($autotable_sw, y)
1664 w, pos_x, pos_y = $autotable.window.pointer
1665 $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]])
1666 update_selected.call
1671 ! $autotable.window.nil?
1675 $autotable.signal_connect('button-press-event') { |w,e|
1677 if !$button1_pressed_autotable
1680 if e.state & Gdk::Window::SHIFT_MASK == 0
1681 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1682 $selected_elements = {}
1683 $statusbar.push(0, utf8(_("Nothing selected.")))
1685 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1687 set_mousecursor(Gdk::Cursor::TCROSS)
1691 $autotable.signal_connect('button-release-event') { |w,e|
1693 if $button1_pressed_autotable
1694 #- unselect all only now
1695 $multiple_dnd = $selected_elements.keys
1696 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1697 $selected_elements = {}
1698 $button1_pressed_autotable = false
1701 $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]])
1702 if $selected_elements.length > 0
1703 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1706 press_x = press_y = pos_x = pos_y = nil
1707 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1711 $autotable.signal_connect('motion-notify-event') { |w,e|
1714 $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]])
1718 $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]])
1719 update_selected.call
1725 def create_subalbums_page
1727 subalbums_hb = Gtk::HBox.new
1728 $subalbums_vb = Gtk::VBox.new(false, 5)
1729 subalbums_hb.pack_start($subalbums_vb, false, false)
1730 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1731 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1732 $subalbums_sw.add_with_viewport(subalbums_hb)
1735 def save_current_file
1741 ios = File.open($filename, "w")
1742 $xmldoc.write(ios, 0)
1744 rescue Iconv::IllegalSequence
1745 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1746 if ! ios.nil? && ! ios.closed?
1749 $xmldoc.xml_decl.encoding = 'UTF-8'
1750 ios = File.open($filename, "w")
1751 $xmldoc.write(ios, 0)
1762 def save_current_file_user
1763 save_tempfilename = $filename
1764 $filename = $orig_filename
1765 if ! save_current_file
1766 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1767 $filename = save_tempfilename
1771 $generated_outofline = false
1772 $filename = save_tempfilename
1774 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1775 $todelete.each { |f|
1776 system("rm -f #{f}")
1780 def mark_document_as_dirty
1781 $xmldoc.elements.each('//dir') { |elem|
1782 elem.delete_attribute('already-generated')
1786 #- ret: true => ok false => cancel
1787 def ask_save_modifications(msg1, msg2, *options)
1789 options = options.size > 0 ? options[0] : {}
1791 if options[:disallow_cancel]
1792 dialog = Gtk::Dialog.new(msg1,
1794 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1795 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1796 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1798 dialog = Gtk::Dialog.new(msg1,
1800 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1801 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1802 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1803 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1805 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1806 dialog.vbox.add(Gtk::Label.new(msg2))
1807 dialog.window_position = Gtk::Window::POS_CENTER
1810 dialog.run { |response|
1812 if response == Gtk::Dialog::RESPONSE_YES
1813 if ! save_current_file_user
1814 return ask_save_modifications(msg1, msg2, options)
1817 #- if we have generated an album but won't save modifications, we must remove
1818 #- already-generated markers in original file
1819 if $generated_outofline
1821 $xmldoc = REXML::Document.new File.new($orig_filename)
1822 mark_document_as_dirty
1823 ios = File.open($orig_filename, "w")
1824 $xmldoc.write(ios, 0)
1827 puts "exception: #{$!}"
1831 if response == Gtk::Dialog::RESPONSE_CANCEL
1834 $todelete = [] #- unconditionally clear the list of images/videos to delete
1840 def try_quit(*options)
1841 if ask_save_modifications(utf8(_("Save before quitting?")),
1842 utf8(_("Do you want to save your changes before quitting?")),
1848 def show_popup(parent, msg, *options)
1849 dialog = Gtk::Dialog.new
1850 if options[0] && options[0][:title]
1851 dialog.title = options[0][:title]
1853 dialog.title = utf8(_("Booh message"))
1855 lbl = Gtk::Label.new
1856 if options[0] && options[0][:nomarkup]
1861 if options[0] && options[0][:centered]
1862 lbl.set_justify(Gtk::Justification::CENTER)
1864 if options[0] && options[0][:selectable]
1865 lbl.selectable = true
1867 if options[0] && options[0][:topwidget]
1868 dialog.vbox.add(options[0][:topwidget])
1870 if options[0] && options[0][:scrolled]
1871 sw = Gtk::ScrolledWindow.new(nil, nil)
1872 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1873 sw.add_with_viewport(lbl)
1875 dialog.set_default_size(500, 600)
1877 dialog.vbox.add(lbl)
1878 dialog.set_default_size(200, 120)
1880 if options[0] && options[0][:okcancel]
1881 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1883 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1885 if options[0] && options[0][:pos_centered]
1886 dialog.window_position = Gtk::Window::POS_CENTER
1888 dialog.window_position = Gtk::Window::POS_MOUSE
1891 if options[0] && options[0][:linkurl]
1892 linkbut = Gtk::Button.new('')
1893 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1894 linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1895 linkbut.relief = Gtk::RELIEF_NONE
1896 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1897 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1898 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1903 if !options[0] || !options[0][:not_transient]
1904 dialog.transient_for = parent
1905 dialog.run { |response|
1907 if options[0] && options[0][:okcancel]
1908 return response == Gtk::Dialog::RESPONSE_OK
1912 dialog.signal_connect('response') { dialog.destroy }
1916 def backend_wait_message(parent, msg, infopipe_path, mode)
1918 w.set_transient_for(parent)
1921 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1922 vb.pack_start(Gtk::Label.new(msg), false, false)
1924 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1925 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1926 if mode != 'one dir scan'
1927 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1929 if mode == 'web-album'
1930 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1931 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1933 vb.pack_start(Gtk::HSeparator.new, false, false)
1935 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1936 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1937 vb.pack_end(bottom, false, false)
1939 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1940 refresh_thread = Thread.new {
1941 directories_counter = 0
1942 while line = infopipe.gets
1943 if line =~ /^directories: (\d+), sizes: (\d+)/
1944 directories = $1.to_f + 1
1946 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1947 elements = $3.to_f + 1
1948 if mode == 'web-album'
1952 gtk_thread_protect { pb1_1.fraction = 0 }
1953 if mode != 'one dir scan'
1954 newtext = utf8(full_src_dir_to_rel($1, $2))
1955 newtext = '/' if newtext == ''
1956 gtk_thread_protect { pb1_2.text = newtext }
1957 directories_counter += 1
1958 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1960 elsif line =~ /^processing element$/
1961 element_counter += 1
1962 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1963 elsif line =~ /^processing size$/
1964 element_counter += 1
1965 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1966 elsif line =~ /^finished processing sizes$/
1967 gtk_thread_protect { pb1_1.fraction = 1 }
1968 elsif line =~ /^creating index.html$/
1969 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1970 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1971 directories_counter = 0
1972 elsif line =~ /^index.html: (.+)\|(.+)/
1973 newtext = utf8(full_src_dir_to_rel($1, $2))
1974 newtext = '/' if newtext == ''
1975 gtk_thread_protect { pb2.text = newtext }
1976 directories_counter += 1
1977 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1978 elsif line =~ /^die: (.*)$/
1985 w.signal_connect('delete-event') { w.destroy }
1986 w.signal_connect('destroy') {
1987 Thread.kill(refresh_thread)
1988 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1991 system("rm -f #{infopipe_path}")
1994 w.window_position = Gtk::Window::POS_CENTER
2000 def call_backend(cmd, waitmsg, mode, params)
2001 pipe = Tempfile.new("boohpipe")
2003 system("mkfifo #{pipe.path}")
2004 cmd += " --info-pipe #{pipe.path}"
2005 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2010 id, exitstatus = Process.waitpid2(pid)
2011 gtk_thread_protect { w8.destroy }
2013 if params[:successmsg]
2014 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2016 if params[:closure_after]
2017 gtk_thread_protect(¶ms[:closure_after])
2019 elsif exitstatus == 15
2020 #- say nothing, user aborted
2022 gtk_thread_protect { show_popup($main_window,
2023 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2029 button.signal_connect('clicked') {
2030 Process.kill('SIGTERM', pid)
2034 def save_changes(*forced)
2035 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2039 $xmldir.delete_attribute('already-generated')
2041 propagate_children = proc { |xmldir|
2042 if xmldir.attributes['subdirs-caption']
2043 xmldir.delete_attribute('already-generated')
2045 xmldir.elements.each('dir') { |element|
2046 propagate_children.call(element)
2050 if $xmldir.child_byname_notattr('dir', 'deleted')
2051 new_title = $subalbums_title.buffer.text
2052 if new_title != $xmldir.attributes['subdirs-caption']
2053 parent = $xmldir.parent
2054 if parent.name == 'dir'
2055 parent.delete_attribute('already-generated')
2057 propagate_children.call($xmldir)
2059 $xmldir.add_attribute('subdirs-caption', new_title)
2060 $xmldir.elements.each('dir') { |element|
2061 if !element.attributes['deleted']
2062 path = element.attributes['path']
2063 newtext = $subalbums_edits[path][:editzone].buffer.text
2064 if element.attributes['subdirs-caption']
2065 if element.attributes['subdirs-caption'] != newtext
2066 propagate_children.call(element)
2068 element.add_attribute('subdirs-caption', newtext)
2069 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2071 if element.attributes['thumbnails-caption'] != newtext
2072 element.delete_attribute('already-generated')
2074 element.add_attribute('thumbnails-caption', newtext)
2075 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2081 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2082 if $xmldir.attributes['thumbnails-caption']
2083 path = $xmldir.attributes['path']
2084 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2086 elsif $xmldir.attributes['thumbnails-caption']
2087 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2090 if $xmldir.attributes['thumbnails-caption']
2091 if edit = $subalbums_edits[$xmldir.attributes['path']]
2092 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2096 #- remove and reinsert elements to reflect new ordering
2099 $xmldir.elements.each { |element|
2100 if element.name == 'image' || element.name == 'video'
2101 saves[element.attributes['filename']] = element.remove
2105 $autotable.current_order.each { |path|
2106 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2107 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2110 saves.each_key { |path|
2111 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2112 chld.add_attribute('deleted', 'true')
2116 def sort_by_exif_date
2120 $xmldir.elements.each { |element|
2121 if element.name == 'image' || element.name == 'video'
2122 current_order << element.attributes['filename']
2126 #- look for EXIF dates
2128 w.set_transient_for($main_window)
2130 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2131 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2132 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2133 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2134 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2135 vb.pack_end(bottom, false, false)
2137 w.signal_connect('delete-event') { w.destroy }
2138 w.window_position = Gtk::Window::POS_CENTER
2142 b.signal_connect('clicked') { aborted = true }
2145 current_order.each { |f|
2147 if entry2type(f) == 'image'
2149 pb.fraction = i.to_f / current_order.size
2150 Gtk.main_iteration while Gtk.events_pending?
2151 date_time = `identify -format "%[EXIF:DateTimeOriginal]" '#{from_utf8($current_path + "/" + f)}'`.chomp
2152 if $? == 0 && date_time != ''
2153 dates[f] = date_time
2166 $xmldir.elements.each { |element|
2167 if element.name == 'image' || element.name == 'video'
2168 saves[element.attributes['filename']] = element.remove
2172 #- find a good fallback for all entries without a date (still next to the item they were next to)
2173 neworder = dates.keys.sort { |a,b| dates[a] <=> dates[b] }
2174 for i in 0 .. current_order.size - 1
2175 if ! neworder.include?(current_order[i])
2177 while j > 0 && ! neworder.include?(current_order[j])
2180 neworder[(neworder.index(current_order[j]) || -1 ) + 1, 0] = current_order[i]
2184 $xmldir.add_element(saves[f].name, saves[f].attributes)
2187 #- let the auto-table reflect new ordering
2191 def remove_all_captions
2194 $autotable.current_order.each { |path|
2195 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2196 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2198 save_undo(_("remove all captions"),
2200 texts.each_key { |key|
2201 $name2widgets[key][:textview].buffer.text = texts[key]
2203 $notebook.set_page(1)
2205 texts.each_key { |key|
2206 $name2widgets[key][:textview].buffer.text = ''
2208 $notebook.set_page(1)
2214 $selected_elements.each_key { |path|
2215 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2221 $selected_elements = {}
2225 $undo_tb.sensitive = $undo_mb.sensitive = false
2226 $redo_tb.sensitive = $redo_mb.sensitive = false
2232 $subalbums_vb.children.each { |chld|
2233 $subalbums_vb.remove(chld)
2235 $subalbums = Gtk::Table.new(0, 0, true)
2236 current_y_sub_albums = 0
2238 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2239 $subalbums_edits = {}
2240 subalbums_counter = 0
2241 subalbums_edits_bypos = {}
2243 add_subalbum = proc { |xmldir, counter|
2244 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2245 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2246 if xmldir == $xmldir
2247 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2248 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2249 caption = xmldir.attributes['thumbnails-caption']
2250 infotype = 'thumbnails'
2252 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2253 captionfile, caption = find_subalbum_caption_info(xmldir)
2254 infotype = find_subalbum_info_type(xmldir)
2256 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2257 hbox = Gtk::HBox.new
2258 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2260 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2263 my_gen_real_thumbnail = proc {
2264 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2267 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2268 f.add(img = Gtk::Image.new)
2269 my_gen_real_thumbnail.call
2271 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2273 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2274 $subalbums.attach(hbox,
2275 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2277 frame, textview = create_editzone($subalbums_sw, 0, img)
2278 textview.buffer.text = caption
2279 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2280 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2282 change_image = proc {
2283 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2285 Gtk::FileChooser::ACTION_OPEN,
2287 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2288 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2289 fc.transient_for = $main_window
2290 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))
2291 f.add(preview_img = Gtk::Image.new)
2293 fc.signal_connect('update-preview') { |w|
2295 if fc.preview_filename
2296 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2297 fc.preview_widget_active = true
2299 rescue Gdk::PixbufError
2300 fc.preview_widget_active = false
2303 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2305 old_file = captionfile
2306 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2307 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2308 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2309 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2311 new_file = fc.filename
2312 msg 3, "new captionfile is: #{fc.filename}"
2313 perform_changefile = proc {
2314 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2315 $modified_pixbufs.delete(thumbnail_file)
2316 xmldir.delete_attribute("#{infotype}-rotate")
2317 xmldir.delete_attribute("#{infotype}-color-swap")
2318 xmldir.delete_attribute("#{infotype}-enhance")
2319 xmldir.delete_attribute("#{infotype}-frame-offset")
2320 my_gen_real_thumbnail.call
2322 perform_changefile.call
2324 save_undo(_("change caption file for sub-album"),
2326 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2327 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2328 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2329 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2330 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2331 my_gen_real_thumbnail.call
2332 $notebook.set_page(0)
2334 perform_changefile.call
2335 $notebook.set_page(0)
2343 system("rm -f '#{thumbnail_file}'")
2344 my_gen_real_thumbnail.call
2347 rotate_and_cleanup = proc { |angle|
2348 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2349 system("rm -f '#{thumbnail_file}'")
2352 move = proc { |direction|
2355 save_changes('forced')
2356 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2357 if direction == 'up'
2358 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2359 subalbums_edits_bypos[oldpos - 1][:position] += 1
2361 if direction == 'down'
2362 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2363 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2365 if direction == 'top'
2366 for i in 1 .. oldpos - 1
2367 subalbums_edits_bypos[i][:position] += 1
2369 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2371 if direction == 'bottom'
2372 for i in oldpos + 1 .. subalbums_counter
2373 subalbums_edits_bypos[i][:position] -= 1
2375 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2379 $xmldir.elements.each('dir') { |element|
2380 if (!element.attributes['deleted'])
2381 elems << [ element.attributes['path'], element.remove ]
2384 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2385 each { |e| $xmldir.add_element(e[1]) }
2386 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2387 $xmldir.elements.each('descendant::dir') { |elem|
2388 elem.delete_attribute('already-generated')
2391 sel = $albums_tv.selection.selected_rows
2393 populate_subalbums_treeview(false)
2394 $albums_tv.selection.select_path(sel[0])
2397 color_swap_and_cleanup = proc {
2398 perform_color_swap_and_cleanup = proc {
2399 color_swap(xmldir, "#{infotype}-")
2400 my_gen_real_thumbnail.call
2402 perform_color_swap_and_cleanup.call
2404 save_undo(_("color swap"),
2406 perform_color_swap_and_cleanup.call
2407 $notebook.set_page(0)
2409 perform_color_swap_and_cleanup.call
2410 $notebook.set_page(0)
2415 change_frame_offset_and_cleanup = proc {
2416 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2417 perform_change_frame_offset_and_cleanup = proc { |val|
2418 change_frame_offset(xmldir, "#{infotype}-", val)
2419 my_gen_real_thumbnail.call
2421 perform_change_frame_offset_and_cleanup.call(values[:new])
2423 save_undo(_("specify frame offset"),
2425 perform_change_frame_offset_and_cleanup.call(values[:old])
2426 $notebook.set_page(0)
2428 perform_change_frame_offset_and_cleanup.call(values[:new])
2429 $notebook.set_page(0)
2435 whitebalance_and_cleanup = proc {
2436 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2437 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2438 perform_change_whitebalance_and_cleanup = proc { |val|
2439 change_whitebalance(xmldir, "#{infotype}-", val)
2440 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2441 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2442 system("rm -f '#{thumbnail_file}'")
2444 perform_change_whitebalance_and_cleanup.call(values[:new])
2446 save_undo(_("fix white balance"),
2448 perform_change_whitebalance_and_cleanup.call(values[:old])
2449 $notebook.set_page(0)
2451 perform_change_whitebalance_and_cleanup.call(values[:new])
2452 $notebook.set_page(0)
2458 gammacorrect_and_cleanup = proc {
2459 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2460 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2461 perform_change_gammacorrect_and_cleanup = proc { |val|
2462 change_gammacorrect(xmldir, "#{infotype}-", val)
2463 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2464 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2465 system("rm -f '#{thumbnail_file}'")
2467 perform_change_gammacorrect_and_cleanup.call(values[:new])
2469 save_undo(_("gamma correction"),
2471 perform_change_gammacorrect_and_cleanup.call(values[:old])
2472 $notebook.set_page(0)
2474 perform_change_gammacorrect_and_cleanup.call(values[:new])
2475 $notebook.set_page(0)
2481 enhance_and_cleanup = proc {
2482 perform_enhance_and_cleanup = proc {
2483 enhance(xmldir, "#{infotype}-")
2484 my_gen_real_thumbnail.call
2487 perform_enhance_and_cleanup.call
2489 save_undo(_("enhance"),
2491 perform_enhance_and_cleanup.call
2492 $notebook.set_page(0)
2494 perform_enhance_and_cleanup.call
2495 $notebook.set_page(0)
2500 evtbox.signal_connect('button-press-event') { |w, event|
2501 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2503 rotate_and_cleanup.call(90)
2505 rotate_and_cleanup.call(-90)
2506 elsif $enhance.active?
2507 enhance_and_cleanup.call
2510 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2511 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2512 { :forbid_left => true, :forbid_right => true,
2513 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2514 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2515 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2516 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2517 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2519 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2524 evtbox.signal_connect('button-press-event') { |w, event|
2525 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2529 evtbox.signal_connect('button-release-event') { |w, event|
2530 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2531 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2532 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2533 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2534 msg 3, "gesture rotate: #{angle}"
2535 rotate_and_cleanup.call(angle)
2538 $gesture_press = nil
2541 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2542 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2543 current_y_sub_albums += 1
2546 if $xmldir.child_byname_notattr('dir', 'deleted')
2548 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2549 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2550 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2551 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2552 #- this album image/caption
2553 if $xmldir.attributes['thumbnails-caption']
2554 add_subalbum.call($xmldir, 0)
2557 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2558 $xmldir.elements.each { |element|
2559 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2560 #- element (image or video) of this album
2561 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2562 msg 3, "dest_img: #{dest_img}"
2563 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2564 total[element.name] += 1
2566 if element.name == 'dir' && !element.attributes['deleted']
2567 #- sub-album image/caption
2568 add_subalbum.call(element, subalbums_counter += 1)
2569 total[element.name] += 1
2572 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2573 total['image'], total['video'], total['dir'] ]))
2574 $subalbums_vb.add($subalbums)
2575 $subalbums_vb.show_all
2577 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2578 $notebook.get_tab_label($autotable_sw).sensitive = false
2579 $notebook.set_page(0)
2580 $thumbnails_title.buffer.text = ''
2582 $notebook.get_tab_label($autotable_sw).sensitive = true
2583 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2586 if !$xmldir.child_byname_notattr('dir', 'deleted')
2587 $notebook.get_tab_label($subalbums_sw).sensitive = false
2588 $notebook.set_page(1)
2590 $notebook.get_tab_label($subalbums_sw).sensitive = true
2594 def pixbuf_or_nil(filename)
2596 return Gdk::Pixbuf.new(filename)
2602 def theme_choose(current)
2603 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2605 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2606 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2607 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2609 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2610 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2611 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2612 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2613 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2614 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2615 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2616 treeview.signal_connect('button-press-event') { |w, event|
2617 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2618 dialog.response(Gtk::Dialog::RESPONSE_OK)
2622 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2624 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2627 iter[0] = File.basename(dir)
2628 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2629 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2630 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2631 if File.basename(dir) == current
2632 treeview.selection.select_iter(iter)
2636 dialog.set_default_size(700, 400)
2637 dialog.vbox.show_all
2638 dialog.run { |response|
2639 iter = treeview.selection.selected
2641 if response == Gtk::Dialog::RESPONSE_OK && iter
2642 return model.get_value(iter, 0)
2648 def show_password_protections
2649 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2650 child_iter = $albums_iters[xmldir.attributes['path']]
2651 if xmldir.attributes['password-protect']
2652 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2653 already_protected = true
2654 elsif already_protected
2655 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2657 pix = pix.saturate_and_pixelate(1, true)
2663 xmldir.elements.each('dir') { |elem|
2664 if !elem.attributes['deleted']
2665 examine_dir_elem.call(child_iter, elem, already_protected)
2669 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2672 def populate_subalbums_treeview(select_first)
2676 $subalbums_vb.children.each { |chld|
2677 $subalbums_vb.remove(chld)
2680 source = $xmldoc.root.attributes['source']
2681 msg 3, "source: #{source}"
2683 xmldir = $xmldoc.elements['//dir']
2684 if !xmldir || xmldir.attributes['path'] != source
2685 msg 1, _("Corrupted booh file...")
2689 append_dir_elem = proc { |parent_iter, xmldir|
2690 child_iter = $albums_ts.append(parent_iter)
2691 child_iter[0] = File.basename(xmldir.attributes['path'])
2692 child_iter[1] = xmldir.attributes['path']
2693 $albums_iters[xmldir.attributes['path']] = child_iter
2694 msg 3, "puttin location: #{xmldir.attributes['path']}"
2695 xmldir.elements.each('dir') { |elem|
2696 if !elem.attributes['deleted']
2697 append_dir_elem.call(child_iter, elem)
2701 append_dir_elem.call(nil, xmldir)
2702 show_password_protections
2704 $albums_tv.expand_all
2706 $albums_tv.selection.select_iter($albums_ts.iter_first)
2710 def select_current_theme
2711 select_theme($xmldoc.root.attributes['theme'],
2712 $xmldoc.root.attributes['limit-sizes'],
2713 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2714 $xmldoc.root.attributes['thumbnails-per-row'])
2717 def open_file(filename)
2721 $current_path = nil #- invalidate
2722 $modified_pixbufs = {}
2725 $subalbums_vb.children.each { |chld|
2726 $subalbums_vb.remove(chld)
2729 if !File.exists?(filename)
2730 return utf8(_("File not found."))
2734 $xmldoc = REXML::Document.new File.new(filename)
2739 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2740 if entry2type(filename).nil?
2741 return utf8(_("Not a booh file!"))
2743 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."))
2747 if !source = $xmldoc.root.attributes['source']
2748 return utf8(_("Corrupted booh file..."))
2751 if !dest = $xmldoc.root.attributes['destination']
2752 return utf8(_("Corrupted booh file..."))
2755 if !theme = $xmldoc.root.attributes['theme']
2756 return utf8(_("Corrupted booh file..."))
2759 if $xmldoc.root.attributes['version'] < '0.8.6'
2760 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2761 mark_document_as_dirty
2762 if $xmldoc.root.attributes['version'] < '0.8.4'
2763 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2764 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2765 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2766 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2767 if old_dest_dir != new_dest_dir
2768 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2770 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2771 xmldir.elements.each { |element|
2772 if %w(image video).include?(element.name) && !element.attributes['deleted']
2773 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2774 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2775 Dir[old_name + '*'].each { |file|
2776 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2777 file != new_file and sys("mv '#{file}' '#{new_file}'")
2780 if element.name == 'dir' && !element.attributes['deleted']
2781 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2782 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2783 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2787 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2791 $xmldoc.root.add_attribute('version', $VERSION)
2794 select_current_theme
2796 $filename = filename
2797 $default_size['thumbnails'] =~ /(.*)x(.*)/
2798 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2799 $albums_thumbnail_size =~ /(.*)x(.*)/
2800 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2802 populate_subalbums_treeview(true)
2804 $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
2808 def open_file_user(filename)
2809 result = open_file(filename)
2811 $config['last-opens'] ||= []
2812 if $config['last-opens'][-1] != utf8(filename)
2813 $config['last-opens'] << utf8(filename)
2815 $orig_filename = $filename
2816 tmp = Tempfile.new("boohtemp")
2819 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2821 $tempfiles << $filename << "#{$filename}.backup"
2823 $orig_filename = nil
2829 if !ask_save_modifications(utf8(_("Save this album?")),
2830 utf8(_("Do you want to save the changes to this album?")),
2831 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2834 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2836 Gtk::FileChooser::ACTION_OPEN,
2838 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2839 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2840 fc.set_current_folder(File.expand_path("~/.booh"))
2841 fc.transient_for = $main_window
2844 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2845 push_mousecursor_wait(fc)
2846 msg = open_file_user(fc.filename)
2862 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2867 def additional_booh_options
2870 options += "--mproc #{$config['mproc'].to_i} "
2872 options += "--comments-format '#{$config['comments-format']}'"
2877 if !ask_save_modifications(utf8(_("Save this album?")),
2878 utf8(_("Do you want to save the changes to this album?")),
2879 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2882 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2884 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2885 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2886 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2888 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2889 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2890 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2891 tbl.attach(src = Gtk::Entry.new,
2892 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2893 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2894 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2895 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2896 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2897 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2898 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2899 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2900 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2901 tbl.attach(dest = Gtk::Entry.new,
2902 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2903 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2904 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2905 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2906 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2907 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2908 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2909 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2910 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2912 tooltips = Gtk::Tooltips.new
2913 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2914 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2915 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2916 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2917 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2918 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2919 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)
2920 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2921 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2922 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
2923 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
2924 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)
2925 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2926 pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
2927 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)
2929 src_nb_calculated_for = ''
2931 process_src_nb = proc {
2932 if src.text != src_nb_calculated_for
2933 src_nb_calculated_for = src.text
2935 Thread.kill(src_nb_thread)
2938 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
2939 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
2941 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2942 if File.readable?(from_utf8_safe(src_nb_calculated_for))
2943 src_nb_thread = Thread.new {
2944 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2945 total = { 'image' => 0, 'video' => 0, nil => 0 }
2946 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2947 if File.basename(dir) =~ /^\./
2951 Dir.entries(dir.chomp).each { |file|
2952 total[entry2type(file)] += 1
2954 rescue Errno::EACCES, Errno::ENOENT
2958 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2962 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2965 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2971 timeout_src_nb = Gtk.timeout_add(100) {
2975 src_browse.signal_connect('clicked') {
2976 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2978 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2980 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2981 fc.transient_for = $main_window
2982 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2983 src.text = utf8(fc.filename)
2985 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2990 dest_browse.signal_connect('clicked') {
2991 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2993 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2995 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2996 fc.transient_for = $main_window
2997 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2998 dest.text = utf8(fc.filename)
3003 conf_browse.signal_connect('clicked') {
3004 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3006 Gtk::FileChooser::ACTION_SAVE,
3008 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3009 fc.transient_for = $main_window
3010 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3011 fc.set_current_folder(File.expand_path("~/.booh"))
3012 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3013 conf.text = utf8(fc.filename)
3020 recreate_theme_config = proc {
3021 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3023 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3024 $images_size.each { |s|
3025 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3029 tooltips.set_tip(cb, utf8(s['description']), nil)
3030 theme_sizes << { :widget => cb, :value => s['name'] }
3032 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3033 tooltips = Gtk::Tooltips.new
3034 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3035 theme_sizes << { :widget => cb, :value => 'original' }
3038 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3041 $allowed_N_values.each { |n|
3043 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3045 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3047 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3051 nperrows << { :widget => rb, :value => n }
3053 nperrowradios.show_all
3055 recreate_theme_config.call
3057 theme_button.signal_connect('clicked') {
3058 if newtheme = theme_choose(theme_button.label)
3059 theme_button.label = newtheme
3060 recreate_theme_config.call
3064 dialog.vbox.add(frame1)
3065 dialog.vbox.add(frame2)
3066 dialog.window_position = Gtk::Window::POS_MOUSE
3072 dialog.run { |response|
3073 if response == Gtk::Dialog::RESPONSE_OK
3074 srcdir = from_utf8_safe(src.text)
3075 destdir = from_utf8_safe(dest.text)
3076 confpath = from_utf8_safe(conf.text)
3077 if src.text != '' && srcdir == ''
3078 show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3080 elsif !File.directory?(srcdir)
3081 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3083 elsif dest.text != '' && destdir == ''
3084 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3086 elsif destdir != make_dest_filename(destdir)
3087 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3089 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3090 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
3092 elsif File.exists?(destdir) && !File.directory?(destdir)
3093 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3095 elsif conf.text == ''
3096 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3098 elsif conf.text != '' && confpath == ''
3099 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3101 elsif File.directory?(confpath)
3102 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3104 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3105 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3107 system("mkdir '#{destdir}'")
3108 if !File.directory?(destdir)
3109 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3121 srcdir = from_utf8(src.text)
3122 destdir = from_utf8(dest.text)
3123 configskel = File.expand_path(from_utf8(conf.text))
3124 theme = theme_button.label
3125 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3126 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3127 opt432 = optimize432.active?
3128 madewith = madewithentry.text
3129 indexlink = indexlinkentry.text
3132 Thread.kill(src_nb_thread)
3133 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3136 Gtk.timeout_remove(timeout_src_nb)
3139 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3140 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3141 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3142 utf8(_("Please wait while scanning source directory...")),
3144 { :closure_after => proc { open_file_user(configskel) } })
3149 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3151 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3152 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3153 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3155 source = $xmldoc.root.attributes['source']
3156 dest = $xmldoc.root.attributes['destination']
3157 theme = $xmldoc.root.attributes['theme']
3158 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3159 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3160 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3162 limit_sizes = limit_sizes.split(/,/)
3164 madewith = $xmldoc.root.attributes['made-with']
3165 indexlink = $xmldoc.root.attributes['index-link']
3167 tooltips = Gtk::Tooltips.new
3168 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3169 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3170 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3171 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3172 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3173 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3174 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3175 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3176 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3177 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3178 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3179 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3180 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3182 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3183 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3184 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3185 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3186 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3187 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3188 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)
3189 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3190 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3192 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3193 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3195 indexlinkentry.text = indexlink
3197 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)
3198 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3199 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3201 madewithentry.text = madewith
3203 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)
3207 recreate_theme_config = proc {
3208 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3210 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3212 $images_size.each { |s|
3213 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3215 if limit_sizes.include?(s['name'])
3223 tooltips.set_tip(cb, utf8(s['description']), nil)
3224 theme_sizes << { :widget => cb, :value => s['name'] }
3226 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3227 tooltips = Gtk::Tooltips.new
3228 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3229 if limit_sizes && limit_sizes.include?('original')
3232 theme_sizes << { :widget => cb, :value => 'original' }
3235 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3238 $allowed_N_values.each { |n|
3240 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3242 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3244 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3245 nperrowradios.add(Gtk::Label.new(' '))
3246 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3249 nperrows << { :widget => rb, :value => n.to_s }
3251 nperrowradios.show_all
3253 recreate_theme_config.call
3255 theme_button.signal_connect('clicked') {
3256 if newtheme = theme_choose(theme_button.label)
3259 theme_button.label = newtheme
3260 recreate_theme_config.call
3264 dialog.vbox.add(frame1)
3265 dialog.vbox.add(frame2)
3266 dialog.window_position = Gtk::Window::POS_MOUSE
3272 dialog.run { |response|
3273 if response == Gtk::Dialog::RESPONSE_OK
3274 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3275 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3284 save_theme = theme_button.label
3285 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3286 save_opt432 = optimize432.active?
3287 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3288 save_madewith = madewithentry.text
3289 save_indexlink = indexlinkentry.text
3292 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith || save_indexlink != indexlinkentry)
3293 mark_document_as_dirty
3295 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3296 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3297 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3298 utf8(_("Please wait while scanning source directory...")),
3300 { :closure_after => proc {
3301 open_file($filename)
3305 #- select_theme merges global variables, need to return to current choices
3306 select_current_theme
3313 sel = $albums_tv.selection.selected_rows
3315 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3316 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3317 utf8(_("Please wait while scanning source directory...")),
3319 { :closure_after => proc {
3320 open_file($filename)
3321 $albums_tv.selection.select_path(sel[0])
3329 sel = $albums_tv.selection.selected_rows
3331 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3332 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3333 utf8(_("Please wait while scanning source directory...")),
3335 { :closure_after => proc {
3336 open_file($filename)
3337 $albums_tv.selection.select_path(sel[0])
3345 theme = $xmldoc.root.attributes['theme']
3346 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3348 limit_sizes = "--sizes #{limit_sizes}"
3350 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3351 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3352 utf8(_("Please wait while scanning source directory...")),
3354 { :closure_after => proc {
3355 open_file($filename)
3361 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3363 Gtk::FileChooser::ACTION_SAVE,
3365 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3366 fc.transient_for = $main_window
3367 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3368 fc.set_current_folder(File.expand_path("~/.booh"))
3369 fc.filename = $orig_filename
3370 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3371 $orig_filename = fc.filename
3372 if ! save_current_file_user
3376 $config['last-opens'] ||= []
3377 $config['last-opens'] << $orig_filename
3383 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3385 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3386 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3387 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3389 dialog.vbox.add(notebook = Gtk::Notebook.new)
3390 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3391 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3392 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3393 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)),
3394 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3395 tooltips = Gtk::Tooltips.new
3396 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3397 for example: /usr/bin/mplayer %f")), nil)
3398 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3399 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3400 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3401 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3402 tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3403 for example: /usr/bin/gimp-remote %f")), nil)
3404 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3405 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3406 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3407 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3408 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3409 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3410 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3411 0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3412 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)),
3413 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3414 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)
3415 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3416 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3417 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)
3418 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3419 0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3420 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)
3422 smp_check.signal_connect('toggled') {
3423 if smp_check.active?
3424 smp_hbox.sensitive = true
3426 smp_hbox.sensitive = false
3430 smp_check.active = true
3431 smp_spin.value = $config['mproc'].to_i
3433 nogestures_check.active = $config['nogestures']
3434 deleteondisk_check.active = $config['deleteondisk']
3436 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3437 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3438 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3439 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3440 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3441 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3442 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3443 tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3444 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3445 tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3446 2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3447 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)
3448 commentsformat_help.signal_connect('clicked') {
3449 show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3450 hence you should look at ImageMagick/identify documentation for the most
3451 accurate and up-to-date documentation. Last time I checked, documentation
3454 Print information about the image in a format of your choosing. You can
3455 include the image filename, type, width, height, Exif data, or other image
3456 attributes by embedding special format characters:
3459 %P page width and height
3463 %e filename extension
3468 %k number of unique colors
3475 %r image class and colorspace
3478 %u unique temporary filename