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 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., 675 Mass Ave, Cambridge, MA 02139, USA.
27 require 'booh/gtkadds'
28 require 'booh/GtkAutoTable'
32 bindtextdomain("booh")
34 require 'rexml/document'
37 require 'booh/booh-lib'
39 require 'booh/UndoHandler'
44 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
46 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
49 #- default values for some globals
53 $ignore_videos = false
54 $button1_pressed_autotable = false
55 $generated_outofline = false
58 puts _("Usage: %s [OPTION]...") % File.basename($0)
60 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
65 parser = GetoptLong.new
66 parser.set_options(*$options.collect { |ary| ary[0..2] })
68 parser.each_option do |name, arg|
74 when '--verbose-level'
75 $verbose_level = arg.to_i
88 $config_file = File.expand_path('~/.booh-gui-rc')
89 if File.readable?($config_file)
90 $xmldoc = REXML::Document.new(File.new($config_file))
91 $xmldoc.root.elements.each { |element|
92 txt = element.get_text
94 if txt.value =~ /~~~/ || element.name == 'last-opens'
95 $config[element.name] = txt.value.split(/~~~/)
97 $config[element.name] = txt.value
99 elsif element.elements.size == 0
100 $config[element.name] = ''
102 $config[element.name] = {}
103 element.each { |chld|
105 $config[element.name][chld.name] = txt ? txt.value : nil
110 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
111 $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
112 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
113 $config['comments-format'] ||= '%t'
114 if !FileTest.directory?(File.expand_path('~/.booh'))
115 system("mkdir ~/.booh")
123 if !system("which convert >/dev/null 2>/dev/null")
124 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
125 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
128 if !system("which identify >/dev/null 2>/dev/null")
129 show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
130 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
132 missing = %w(transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
134 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
137 viewer_binary = $config['video-viewer'].split.first
138 if viewer_binary && !File.executable?(viewer_binary)
139 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
140 You should fix this in Edit/Preferences so that you can view videos.
142 Problem was: '%s' is not an executable file.
143 Hint: don't forget to specify the full path to the executable,
144 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
146 image_editor_binary = $config['image-editor'].split.first
147 if image_editor_binary && !File.executable?(image_editor_binary)
148 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
149 You should fix this in Edit/Preferences so that you can edit images externally.
151 Problem was: '%s' is not an executable file.
152 Hint: don't forget to specify the full path to the executable,
153 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
155 browser_binary = $config['browser'].split.first
156 if browser_binary && !File.executable?(browser_binary)
157 show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
158 You should fix this in Edit/Preferences so that you can open URLs.
160 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
165 if $config['last-opens'] && $config['last-opens'].size > 10
166 $config['last-opens'] = $config['last-opens'][-10, 10]
169 ios = File.open($config_file, "w")
170 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
171 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
172 $config.each_pair { |key, value|
173 elem = $xmldoc.root.add_element key
175 $config[key].each_pair { |subkey, subvalue|
176 subelem = elem.add_element subkey
177 subelem.add_text subvalue.to_s
179 elsif value.is_a? Array
180 elem.add_text value.join('~~~')
185 elem.add_text value.to_s
189 $xmldoc.write(ios, 0)
192 $tempfiles.each { |f|
197 def set_mousecursor(what, *widget)
198 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
199 if widget[0] && widget[0].window
200 widget[0].window.cursor = cursor
202 if $main_window && $main_window.window
203 $main_window.window.cursor = cursor
205 $current_cursor = what
207 def set_mousecursor_wait(*widget)
208 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
209 if Thread.current == Thread.main
210 Gtk.main_iteration while Gtk.events_pending?
213 def set_mousecursor_normal(*widget)
214 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
216 def push_mousecursor_wait(*widget)
217 if $current_cursor != Gdk::Cursor::WATCH
218 $save_cursor = $current_cursor
219 gtk_thread_protect { set_mousecursor_wait(*widget) }
222 def pop_mousecursor(*widget)
223 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
227 source = $xmldoc.root.attributes['source']
228 dest = $xmldoc.root.attributes['destination']
229 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
232 def full_src_dir_to_rel(path, source)
233 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
236 def build_full_dest_filename(filename)
237 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
240 def save_undo(name, closure, *params)
241 UndoHandler.save_undo(name, closure, [ *params ])
242 $undo_tb.sensitive = $undo_mb.sensitive = true
243 $redo_tb.sensitive = $redo_mb.sensitive = false
246 def view_element(filename, closures)
247 if entry2type(filename) == 'video'
248 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
254 w = Gtk::Window.new.set_title(filename)
256 msg 3, "filename: #{filename}"
257 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
258 #- typically this file won't exist in case of videos; try with the largest thumbnail around
259 if !File.exists?(dest_img)
260 if entry2type(filename) == 'video'
261 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
262 if not alternatives.empty?
263 dest_img = alternatives[-1]
266 push_mousecursor_wait
267 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
269 if !File.exists?(dest_img)
270 msg 2, _("Could not generate fullscreen thumbnail!")
275 evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(Gtk::Image.new(dest_img)).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
276 evt.signal_connect('button-press-event') { |this, event|
277 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
278 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
280 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
282 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
283 delete_item.signal_connect('activate') {
285 closures[:delete].call
288 menu.popup(nil, nil, event.button, event.time)
291 evt.signal_connect('button-release-event') { |this, event|
293 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
294 msg 3, "gesture delete: click-drag right button to the bottom"
296 closures[:delete].call
297 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
301 tooltips = Gtk::Tooltips.new
302 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
304 w.signal_connect('key-press-event') { |w,event|
305 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
307 closures[:delete].call
311 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
312 b.signal_connect('clicked') { w.destroy }
315 vb.pack_start(evt, false, false)
316 vb.pack_end(bottom, false, false)
319 w.signal_connect('delete-event') { w.destroy }
320 w.window_position = Gtk::Window::POS_CENTER
324 def scroll_upper(scrolledwindow, ypos_top)
325 newval = scrolledwindow.vadjustment.value -
326 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
327 if newval < scrolledwindow.vadjustment.lower
328 newval = scrolledwindow.vadjustment.lower
330 scrolledwindow.vadjustment.value = newval
333 def scroll_lower(scrolledwindow, ypos_bottom)
334 newval = scrolledwindow.vadjustment.value +
335 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
336 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
337 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
339 scrolledwindow.vadjustment.value = newval
342 def autoscroll_if_needed(scrolledwindow, image, textview)
343 #- autoscroll if cursor or image is not visible, if possible
344 if image && image.window || textview.window
345 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
346 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
347 current_miny_visible = scrolledwindow.vadjustment.value
348 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
349 if ypos_top < current_miny_visible
350 scroll_upper(scrolledwindow, ypos_top)
351 elsif ypos_bottom > current_maxy_visible
352 scroll_lower(scrolledwindow, ypos_bottom)
357 def create_editzone(scrolledwindow, pagenum, image)
358 frame = Gtk::Frame.new
359 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
360 frame.set_shadow_type(Gtk::SHADOW_IN)
361 textview.signal_connect('key-press-event') { |w, event|
362 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
363 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
364 scrolledwindow.signal_emit('key-press-event', event)
366 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
367 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
368 if event.keyval == Gdk::Keyval::GDK_Up
369 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
370 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
372 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
375 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
376 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
378 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
385 candidate_undo_text = nil
386 textview.signal_connect('focus-in-event') { |w, event|
387 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
388 candidate_undo_text = textview.buffer.text
392 textview.signal_connect('key-release-event') { |w, event|
393 if candidate_undo_text && candidate_undo_text != textview.buffer.text
395 save_undo(_("text edit"),
397 save_text = textview.buffer.text
398 textview.buffer.text = text
400 $notebook.set_page(pagenum)
402 textview.buffer.text = save_text
404 $notebook.set_page(pagenum)
406 }, candidate_undo_text)
407 candidate_undo_text = nil
410 if event.state != 0 || ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
411 autoscroll_if_needed(scrolledwindow, image, textview)
416 return [ frame, textview ]
419 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
421 if !$modified_pixbufs[thumbnail_img]
422 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
423 elsif !$modified_pixbufs[thumbnail_img][:orig]
424 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
427 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
430 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
431 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
432 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
433 if pixbuf.height > desired_y
434 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
435 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
436 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
441 if $modified_pixbufs[thumbnail_img][:whitebalance]
442 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
445 #- fix gamma correction
446 if $modified_pixbufs[thumbnail_img][:gammacorrect]
447 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
450 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
453 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
456 #- update rotate attribute
457 xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
459 $modified_pixbufs[thumbnail_img] ||= {}
460 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
461 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
463 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
466 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
469 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
471 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
473 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
474 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
476 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
477 $notebook.set_page(0)
478 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
483 def color_swap(xmldir, attributes_prefix)
485 if xmldir.attributes["#{attributes_prefix}color-swap"]
486 xmldir.delete_attribute("#{attributes_prefix}color-swap")
488 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
492 def enhance(xmldir, attributes_prefix)
494 if xmldir.attributes["#{attributes_prefix}enhance"]
495 xmldir.delete_attribute("#{attributes_prefix}enhance")
497 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
501 def change_frame_offset(xmldir, attributes_prefix, value)
503 xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
506 def ask_new_frame_offset(xmldir, attributes_prefix)
508 value = xmldir.attributes["#{attributes_prefix}frame-offset"]
513 dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
515 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
516 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
517 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
521 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
522 from. There are approximately 25 frames per second in a video.
525 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
526 entry.signal_connect('key-press-event') { |w, event|
527 if event.keyval == Gdk::Keyval::GDK_Return
528 dialog.response(Gtk::Dialog::RESPONSE_OK)
530 elsif event.keyval == Gdk::Keyval::GDK_Escape
531 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
534 false #- propagate if needed
538 dialog.window_position = Gtk::Window::POS_MOUSE
541 dialog.run { |response|
544 if response == Gtk::Dialog::RESPONSE_OK
546 msg 3, "changing frame offset to #{newval}"
547 return { :old => value, :new => newval }
554 def change_pano_amount(xmldir, attributes_prefix, value)
557 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
559 xmldir.add_attribute("#{attributes_prefix}pano-amount", value)
563 def ask_new_pano_amount(xmldir, attributes_prefix)
565 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
570 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
572 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
573 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
574 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
578 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
579 of this panorama image compared to other regular images. For example, if the panorama
580 was taken out of four photos on one row, counting the necessary overlap, the width of
581 this panorama image should probably be roughly three times the width of regular images.
583 With this information, booh will be able to generate panorama thumbnails looking
587 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("none (not a panorama image)")))).
588 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
589 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
590 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
591 dialog.window_position = Gtk::Window::POS_MOUSE
594 spin.value = value.to_f
601 dialog.run { |response|
605 newval = spin.value.to_f
608 if response == Gtk::Dialog::RESPONSE_OK
610 msg 3, "changing panorama amount to #{newval}"
611 return { :old => value, :new => newval }
618 def change_whitebalance(xmlelem, attributes_prefix, value)
620 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
623 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
625 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
626 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
627 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
628 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
629 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
630 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
631 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
632 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
633 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
634 $modified_pixbufs[thumbnail_img] ||= {}
635 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
636 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
638 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
639 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
641 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
644 $modified_pixbufs[thumbnail_img] ||= {}
645 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
647 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
650 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
651 #- init $modified_pixbufs correctly
652 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
654 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
656 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
658 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
659 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
660 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
664 _("You can fix the <b>white balance</b> of the image, if your image is too blue
665 or too yellow because your camera didn't detect the light correctly. Drag the
666 slider below the image to the left for more blue, to the right for more yellow.
670 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
672 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
674 dialog.window_position = Gtk::Window::POS_MOUSE
678 timeout = Gtk.timeout_add(100) {
679 if hs.value != lastval
682 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
688 dialog.run { |response|
689 Gtk.timeout_remove(timeout)
690 if response == Gtk::Dialog::RESPONSE_OK
692 newval = hs.value.to_s
693 msg 3, "changing white balance to #{newval}"
695 return { :old => value, :new => newval }
698 $modified_pixbufs[thumbnail_img] ||= {}
699 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
700 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
708 def change_gammacorrect(xmlelem, attributes_prefix, value)
710 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
713 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
715 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
716 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
717 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
718 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
719 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
720 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
721 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
722 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
723 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
724 $modified_pixbufs[thumbnail_img] ||= {}
725 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
726 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
728 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
729 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
731 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
734 $modified_pixbufs[thumbnail_img] ||= {}
735 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
737 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
740 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
741 #- init $modified_pixbufs correctly
742 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
744 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
746 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
748 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
749 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
750 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
754 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
755 or too bright. Drag the slider below the image.
759 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
761 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
763 dialog.window_position = Gtk::Window::POS_MOUSE
767 timeout = Gtk.timeout_add(100) {
768 if hs.value != lastval
771 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
777 dialog.run { |response|
778 Gtk.timeout_remove(timeout)
779 if response == Gtk::Dialog::RESPONSE_OK
781 newval = hs.value.to_s
782 msg 3, "gamma correction to #{newval}"
784 return { :old => value, :new => newval }
787 $modified_pixbufs[thumbnail_img] ||= {}
788 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
789 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
797 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
798 system("rm -f '#{destfile}'")
799 #- type can be 'element' or 'subdir'
801 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
803 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
807 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
809 push_mousecursor_wait
810 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
813 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
819 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
820 distribute_multiple_call = Proc.new { |action, arg|
821 $selected_elements.each_key { |path|
822 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
824 if possible_actions[:can_multiple] && $selected_elements.length > 0
825 UndoHandler.begin_batch
826 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
827 UndoHandler.end_batch
829 closures[action].call(arg)
831 $selected_elements = {}
834 if optionals.include?('change_image')
835 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
836 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
837 changeimg.signal_connect('activate') { closures[:change].call }
838 menu.append(Gtk::SeparatorMenuItem.new)
840 if !possible_actions[:can_multiple] || $selected_elements.length == 0
843 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
844 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
845 view.signal_connect('activate') { closures[:view].call }
847 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
848 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
849 view.signal_connect('activate') { closures[:view].call }
850 menu.append(Gtk::SeparatorMenuItem.new)
853 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
854 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
855 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
856 exif.signal_connect('activate') { show_popup($main_window,
857 utf8(`identify -format "%[EXIF:*]" #{fullpath}`.sub(/MakerNote.*\n/, '')),
858 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
859 menu.append(Gtk::SeparatorMenuItem.new)
862 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
863 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
864 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
865 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
866 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
867 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
868 if !possible_actions[:can_multiple] || $selected_elements.length == 0
869 menu.append(Gtk::SeparatorMenuItem.new)
870 if !possible_actions[:forbid_left]
871 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
872 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
873 moveleft.signal_connect('activate') { closures[:move].call('left') }
874 if !possible_actions[:can_left]
875 moveleft.sensitive = false
878 if !possible_actions[:forbid_right]
879 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
880 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
881 moveright.signal_connect('activate') { closures[:move].call('right') }
882 if !possible_actions[:can_right]
883 moveright.sensitive = false
886 if optionals.include?('move_top')
887 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
888 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
889 movetop.signal_connect('activate') { closures[:move].call('top') }
890 if !possible_actions[:can_top]
891 movetop.sensitive = false
894 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
895 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
896 moveup.signal_connect('activate') { closures[:move].call('up') }
897 if !possible_actions[:can_up]
898 moveup.sensitive = false
900 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
901 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
902 movedown.signal_connect('activate') { closures[:move].call('down') }
903 if !possible_actions[:can_down]
904 movedown.sensitive = false
906 if optionals.include?('move_bottom')
907 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
908 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
909 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
910 if !possible_actions[:can_bottom]
911 movebottom.sensitive = false
916 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
917 menu.append(Gtk::SeparatorMenuItem.new)
918 menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
919 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
920 color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
921 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
922 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
923 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
924 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
925 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
926 frame_offset.signal_connect('activate') {
927 if possible_actions[:can_multiple] && $selected_elements.length > 0
928 if values = ask_new_frame_offset(nil, '')
929 distribute_multiple_call.call(:frame_offset, values)
932 closures[:frame_offset].call
937 menu.append( Gtk::SeparatorMenuItem.new)
938 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
939 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
940 gammacorrect.signal_connect('activate') {
941 if possible_actions[:can_multiple] && $selected_elements.length > 0
942 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
943 distribute_multiple_call.call(:gammacorrect, values)
946 closures[:gammacorrect].call
949 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
950 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
951 whitebalance.signal_connect('activate') {
952 if possible_actions[:can_multiple] && $selected_elements.length > 0
953 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
954 distribute_multiple_call.call(:whitebalance, values)
957 closures[:whitebalance].call
960 if !possible_actions[:can_multiple] || $selected_elements.length == 0
961 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
962 _("Enhance constrast"))))
964 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
966 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
967 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
968 if type == 'image' && possible_actions[:can_panorama]
969 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
970 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
971 panorama.signal_connect('activate') {
972 if possible_actions[:can_multiple] && $selected_elements.length > 0
973 if values = ask_new_pano_amount(nil, '')
974 distribute_multiple_call.call(:pano, values)
977 distribute_multiple_call.call(:pano)
981 menu.append( Gtk::SeparatorMenuItem.new)
982 if optionals.include?('delete')
983 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
984 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
985 if !possible_actions[:can_multiple] || $selected_elements.length == 0
986 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
987 paste_item.signal_connect('activate') { closures[:paste].call }
988 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
989 clear_item.signal_connect('activate') { $cuts = [] }
991 paste_item.sensitive = clear_item.sensitive = false
994 menu.append( Gtk::SeparatorMenuItem.new)
996 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
997 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
998 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
999 editexternally.signal_connect('activate') {
1000 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1005 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1006 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1007 if optionals.include?('delete')
1008 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1009 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1012 menu.popup(nil, nil, event.button, event.time)
1015 def delete_current_subalbum
1017 sel = $albums_tv.selection.selected_rows
1018 $xmldir.elements.each { |e|
1019 if e.name == 'image' || e.name == 'video'
1020 e.add_attribute('deleted', 'true')
1023 #- branch if we have a non deleted subalbum
1024 if $xmldir.child_byname_notattr('dir', 'deleted')
1025 $xmldir.delete_attribute('thumbnails-caption')
1026 $xmldir.delete_attribute('thumbnails-captionfile')
1028 $xmldir.add_attribute('deleted', 'true')
1030 while moveup.parent.name == 'dir'
1031 moveup = moveup.parent
1032 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1033 moveup.add_attribute('deleted', 'true')
1040 save_changes('forced')
1041 populate_subalbums_treeview(false)
1042 $albums_tv.selection.select_path(sel[0])
1048 $current_path = nil #- prevent save_changes from being rerun again
1049 sel = $albums_tv.selection.selected_rows
1050 restore_one = proc { |xmldir|
1051 xmldir.elements.each { |e|
1052 if e.name == 'dir' && e.attributes['deleted']
1055 e.delete_attribute('deleted')
1058 restore_one.call($xmldir)
1059 populate_subalbums_treeview(false)
1060 $albums_tv.selection.select_path(sel[0])
1063 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1066 frame1 = Gtk::Frame.new
1067 fullpath = from_utf8("#{$current_path}/#{filename}")
1069 my_gen_real_thumbnail = proc {
1070 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1074 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1075 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1076 pack_start(img = Gtk::Image.new).
1077 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1078 px, mask = pxb.render_pixmap_and_mask
1079 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1080 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1082 frame1.add(img = Gtk::Image.new)
1085 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1086 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1087 my_gen_real_thumbnail.call
1089 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1092 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1094 tooltips = Gtk::Tooltips.new
1095 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1096 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1098 frame2, textview = create_editzone($autotable_sw, 1, img)
1099 textview.buffer.text = caption
1100 textview.set_justification(Gtk::Justification::CENTER)
1102 vbox = Gtk::VBox.new(false, 5)
1103 vbox.pack_start(evtbox, false, false)
1104 vbox.pack_start(frame2, false, false)
1105 autotable.append(vbox, filename)
1107 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1108 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1110 #- to be able to find widgets by name
1111 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1113 cleanup_all_thumbnails = proc {
1114 #- remove out of sync images
1115 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1116 for sizeobj in $images_size
1117 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
1123 cleanup_all_thumbnails.call
1124 my_gen_real_thumbnail.call
1127 rotate_and_cleanup = proc { |angle|
1128 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1129 cleanup_all_thumbnails.call
1132 move = proc { |direction|
1133 do_method = "move_#{direction}"
1134 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1136 done = autotable.method(do_method).call(vbox)
1137 textview.grab_focus #- because if moving, focus is stolen
1141 save_undo(_("move %s") % direction,
1143 autotable.method(undo_method).call(vbox)
1144 textview.grab_focus #- because if moving, focus is stolen
1145 autoscroll_if_needed($autotable_sw, img, textview)
1146 $notebook.set_page(1)
1148 autotable.method(do_method).call(vbox)
1149 textview.grab_focus #- because if moving, focus is stolen
1150 autoscroll_if_needed($autotable_sw, img, textview)
1151 $notebook.set_page(1)
1157 color_swap_and_cleanup = proc {
1158 perform_color_swap_and_cleanup = proc {
1159 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1160 my_gen_real_thumbnail.call
1163 cleanup_all_thumbnails.call
1164 perform_color_swap_and_cleanup.call
1166 save_undo(_("color swap"),
1168 perform_color_swap_and_cleanup.call
1170 autoscroll_if_needed($autotable_sw, img, textview)
1171 $notebook.set_page(1)
1173 perform_color_swap_and_cleanup.call
1175 autoscroll_if_needed($autotable_sw, img, textview)
1176 $notebook.set_page(1)
1181 change_frame_offset_and_cleanup_real = proc { |values|
1182 perform_change_frame_offset_and_cleanup = proc { |val|
1183 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
1184 my_gen_real_thumbnail.call
1186 perform_change_frame_offset_and_cleanup.call(values[:new])
1188 save_undo(_("specify frame offset"),
1190 perform_change_frame_offset_and_cleanup.call(values[:old])
1192 autoscroll_if_needed($autotable_sw, img, textview)
1193 $notebook.set_page(1)
1195 perform_change_frame_offset_and_cleanup.call(values[:new])
1197 autoscroll_if_needed($autotable_sw, img, textview)
1198 $notebook.set_page(1)
1203 change_frame_offset_and_cleanup = proc {
1204 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
1205 change_frame_offset_and_cleanup_real.call(values)
1209 change_pano_amount_and_cleanup_real = proc { |values|
1210 perform_change_pano_amount_and_cleanup = proc { |val|
1211 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1213 perform_change_pano_amount_and_cleanup.call(values[:new])
1215 save_undo(_("change panorama amount"),
1217 perform_change_pano_amount_and_cleanup.call(values[:old])
1219 autoscroll_if_needed($autotable_sw, img, textview)
1220 $notebook.set_page(1)
1222 perform_change_pano_amount_and_cleanup.call(values[:new])
1224 autoscroll_if_needed($autotable_sw, img, textview)
1225 $notebook.set_page(1)
1230 change_pano_amount_and_cleanup = proc {
1231 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1232 change_pano_amount_and_cleanup_real.call(values)
1236 whitebalance_and_cleanup_real = proc { |values|
1237 perform_change_whitebalance_and_cleanup = proc { |val|
1238 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1239 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1240 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1241 cleanup_all_thumbnails.call
1243 perform_change_whitebalance_and_cleanup.call(values[:new])
1245 save_undo(_("fix white balance"),
1247 perform_change_whitebalance_and_cleanup.call(values[:old])
1249 autoscroll_if_needed($autotable_sw, img, textview)
1250 $notebook.set_page(1)
1252 perform_change_whitebalance_and_cleanup.call(values[:new])
1254 autoscroll_if_needed($autotable_sw, img, textview)
1255 $notebook.set_page(1)
1260 whitebalance_and_cleanup = proc {
1261 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1262 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1263 whitebalance_and_cleanup_real.call(values)
1267 gammacorrect_and_cleanup_real = proc { |values|
1268 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1269 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1270 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1271 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1272 cleanup_all_thumbnails.call
1274 perform_change_gammacorrect_and_cleanup.call(values[:new])
1276 save_undo(_("gamma correction"),
1278 perform_change_gammacorrect_and_cleanup.call(values[:old])
1280 autoscroll_if_needed($autotable_sw, img, textview)
1281 $notebook.set_page(1)
1283 perform_change_gammacorrect_and_cleanup.call(values[:new])
1285 autoscroll_if_needed($autotable_sw, img, textview)
1286 $notebook.set_page(1)
1291 gammacorrect_and_cleanup = Proc.new {
1292 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1293 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1294 gammacorrect_and_cleanup_real.call(values)
1298 enhance_and_cleanup = proc {
1299 perform_enhance_and_cleanup = proc {
1300 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1301 my_gen_real_thumbnail.call
1304 cleanup_all_thumbnails.call
1305 perform_enhance_and_cleanup.call
1307 save_undo(_("enhance"),
1309 perform_enhance_and_cleanup.call
1311 autoscroll_if_needed($autotable_sw, img, textview)
1312 $notebook.set_page(1)
1314 perform_enhance_and_cleanup.call
1316 autoscroll_if_needed($autotable_sw, img, textview)
1317 $notebook.set_page(1)
1322 delete = proc { |isacut|
1323 if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed? This operation cannot be undone.")), { :okcancel => true })
1326 perform_delete = proc {
1327 after = autotable.get_next_widget(vbox)
1329 after = autotable.get_previous_widget(vbox)
1331 if $config['deleteondisk'] && !isacut
1332 msg 3, "scheduling for delete: #{fullpath}"
1333 $todelete << fullpath
1335 autotable.remove(vbox)
1337 $vbox2widgets[after][:textview].grab_focus
1338 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1342 previous_pos = autotable.get_current_number(vbox)
1346 delete_current_subalbum
1348 save_undo(_("delete"),
1350 autotable.reinsert(pos, vbox, filename)
1351 $notebook.set_page(1)
1352 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1354 msg 3, "removing deletion schedule of: #{fullpath}"
1355 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1358 $notebook.set_page(1)
1367 $cuts << { :vbox => vbox, :filename => filename }
1368 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1373 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1376 autotable.queue_draws << proc {
1377 $vbox2widgets[last[:vbox]][:textview].grab_focus
1378 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1380 save_undo(_("paste"),
1382 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1383 $notebook.set_page(1)
1386 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1388 $notebook.set_page(1)
1391 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1396 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1397 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1398 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1399 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1401 textview.signal_connect('key-press-event') { |w, event|
1404 x, y = autotable.get_current_pos(vbox)
1405 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1406 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1407 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1408 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1410 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1411 $vbox2widgets[widget_up][:textview].grab_focus
1418 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1420 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1421 $vbox2widgets[widget_down][:textview].grab_focus
1428 if event.keyval == Gdk::Keyval::GDK_Left
1431 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1438 rotate_and_cleanup.call(-90)
1441 if event.keyval == Gdk::Keyval::GDK_Right
1442 next_ = autotable.get_next_widget(vbox)
1443 if next_ && autotable.get_current_pos(next_)[0] > x
1445 $vbox2widgets[next_][:textview].grab_focus
1452 rotate_and_cleanup.call(90)
1455 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1458 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1459 view_element(filename, { :delete => delete })
1462 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1465 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1469 !propagate #- propagate if needed
1472 $ignore_next_release = false
1473 evtbox.signal_connect('button-press-event') { |w, event|
1474 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1475 if event.state & Gdk::Window::BUTTON3_MASK != 0
1476 #- gesture redo: hold right mouse button then click left mouse button
1477 $config['nogestures'] or perform_redo
1478 $ignore_next_release = true
1480 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1482 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1484 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1485 elsif $enhance.active?
1486 enhance_and_cleanup.call
1487 elsif $delete.active?
1491 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1494 $button1_pressed_autotable = true
1495 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1496 if event.state & Gdk::Window::BUTTON1_MASK != 0
1497 #- gesture undo: hold left mouse button then click right mouse button
1498 $config['nogestures'] or perform_undo
1499 $ignore_next_release = true
1501 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1502 view_element(filename, { :delete => delete })
1507 evtbox.signal_connect('button-release-event') { |w, event|
1508 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1509 if !$ignore_next_release
1510 x, y = autotable.get_current_pos(vbox)
1511 next_ = autotable.get_next_widget(vbox)
1512 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1513 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1514 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1515 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1516 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1517 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1518 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1520 $ignore_next_release = false
1521 $gesture_press = nil
1526 #- handle reordering with drag and drop
1527 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1528 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1529 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1530 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1533 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1535 #- mouse gesture first (dnd disables button-release-event)
1536 if $gesture_press && $gesture_press[:filename] == filename
1537 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1538 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1539 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1540 rotate_and_cleanup.call(angle)
1541 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1543 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1544 msg 3, "gesture delete: click-drag right button to the bottom"
1546 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1551 ctxt.targets.each { |target|
1552 if target.name == 'reorder-elements'
1553 move_dnd = proc { |from,to|
1556 autotable.move(from, to)
1557 save_undo(_("reorder"),
1560 autotable.move(to - 1, from)
1562 autotable.move(to, from + 1)
1564 $notebook.set_page(1)
1566 autotable.move(from, to)
1567 $notebook.set_page(1)
1572 if $multiple_dnd.size == 0
1573 move_dnd.call(selection_data.data.to_i,
1574 autotable.get_current_number(vbox))
1576 UndoHandler.begin_batch
1577 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1579 #- need to update current position between each call
1580 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1581 autotable.get_current_number(vbox))
1583 UndoHandler.end_batch
1594 def create_auto_table
1596 $autotable = Gtk::AutoTable.new(5)
1598 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1599 thumbnails_vb = Gtk::VBox.new(false, 5)
1601 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1602 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1603 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1604 thumbnails_vb.add($autotable)
1606 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1607 $autotable_sw.add_with_viewport(thumbnails_vb)
1609 #- follows stuff for handling multiple elements selection
1610 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1612 update_selected = proc {
1613 $autotable.current_order.each { |path|
1614 w = $name2widgets[path][:evtbox].window
1615 xm = w.position[0] + w.size[0]/2
1616 ym = w.position[1] + w.size[1]/2
1617 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1618 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1619 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1620 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1623 if $selected_elements[path] && ! $selected_elements[path][:keep]
1624 if ((xm < press_x && xm < pos_x || xm > pos_x && xm > press_x) || (ym < press_y && ym < pos_y || ym > pos_y && ym > press_y))
1625 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1626 $selected_elements.delete(path)
1631 $autotable.signal_connect('realize') { |w,e|
1632 gc = Gdk::GC.new($autotable.window)
1633 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1634 gc.function = Gdk::GC::INVERT
1635 #- autoscroll handling for DND and multiple selections
1636 Gtk.timeout_add(100) {
1637 if ! $autotable.window.nil?
1638 w, x, y, mask = $autotable.window.pointer
1639 if mask & Gdk::Window::BUTTON1_MASK != 0
1640 if y < $autotable_sw.vadjustment.value
1642 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1644 if $button1_pressed_autotable || press_x
1645 scroll_upper($autotable_sw, y)
1648 w, pos_x, pos_y = $autotable.window.pointer
1649 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1650 update_selected.call
1653 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1655 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1657 if $button1_pressed_autotable || press_x
1658 scroll_lower($autotable_sw, y)
1661 w, pos_x, pos_y = $autotable.window.pointer
1662 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1663 update_selected.call
1668 ! $autotable.window.nil?
1672 $autotable.signal_connect('button-press-event') { |w,e|
1674 if !$button1_pressed_autotable
1677 if e.state & Gdk::Window::SHIFT_MASK == 0
1678 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1679 $selected_elements = {}
1680 $statusbar.push(0, utf8(_("Nothing selected.")))
1682 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1684 set_mousecursor(Gdk::Cursor::TCROSS)
1688 $autotable.signal_connect('button-release-event') { |w,e|
1690 if $button1_pressed_autotable
1691 #- unselect all only now
1692 $multiple_dnd = $selected_elements.keys
1693 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1694 $selected_elements = {}
1695 $button1_pressed_autotable = false
1698 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1699 if $selected_elements.length > 0
1700 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1703 press_x = press_y = pos_x = pos_y = nil
1704 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1708 $autotable.signal_connect('motion-notify-event') { |w,e|
1711 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1715 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1716 update_selected.call
1722 def create_subalbums_page
1724 subalbums_hb = Gtk::HBox.new
1725 $subalbums_vb = Gtk::VBox.new(false, 5)
1726 subalbums_hb.pack_start($subalbums_vb, false, false)
1727 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1728 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1729 $subalbums_sw.add_with_viewport(subalbums_hb)
1732 def save_current_file
1738 ios = File.open($filename, "w")
1739 $xmldoc.write(ios, 0)
1741 rescue Iconv::IllegalSequence
1742 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1743 if ! ios.nil? && ! ios.closed?
1746 $xmldoc.xml_decl.encoding = 'UTF-8'
1747 ios = File.open($filename, "w")
1748 $xmldoc.write(ios, 0)
1758 def save_current_file_user
1759 save_tempfilename = $filename
1760 $filename = $orig_filename
1761 if ! save_current_file
1762 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1763 $filename = save_tempfilename
1767 $generated_outofline = false
1768 $filename = save_tempfilename
1770 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1771 $todelete.each { |f|
1772 system("rm -f #{f}")
1776 def mark_document_as_dirty
1777 $xmldoc.elements.each('//dir') { |elem|
1778 elem.delete_attribute('already-generated')
1782 #- ret: true => ok false => cancel
1783 def ask_save_modifications(msg1, msg2, *options)
1785 options = options.size > 0 ? options[0] : {}
1787 if options[:disallow_cancel]
1788 dialog = Gtk::Dialog.new(msg1,
1790 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1791 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1792 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1794 dialog = Gtk::Dialog.new(msg1,
1796 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1797 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1798 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1799 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1801 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1802 dialog.vbox.add(Gtk::Label.new(msg2))
1803 dialog.window_position = Gtk::Window::POS_CENTER
1806 dialog.run { |response|
1808 if response == Gtk::Dialog::RESPONSE_YES
1809 if ! save_current_file_user
1810 return ask_save_modifications(msg1, msg2, options)
1813 #- if we have generated an album but won't save modifications, we must remove
1814 #- already-generated markers in original file
1815 if $generated_outofline
1817 $xmldoc = REXML::Document.new File.new($orig_filename)
1818 mark_document_as_dirty
1819 ios = File.open($orig_filename, "w")
1820 $xmldoc.write(ios, 0)
1823 puts "exception: #{$!}"
1827 if response == Gtk::Dialog::RESPONSE_CANCEL
1830 $todelete = [] #- unconditionally clear the list of images/videos to delete
1836 def try_quit(*options)
1837 if ask_save_modifications(utf8(_("Save before quitting?")),
1838 utf8(_("Do you want to save your changes before quitting?")),
1844 def show_popup(parent, msg, *options)
1845 dialog = Gtk::Dialog.new
1846 if options[0] && options[0][:title]
1847 dialog.title = options[0][:title]
1849 dialog.title = utf8(_("Booh message"))
1851 lbl = Gtk::Label.new
1852 if options[0] && options[0][:nomarkup]
1857 if options[0] && options[0][:centered]
1858 lbl.set_justify(Gtk::Justification::CENTER)
1860 if options[0] && options[0][:selectable]
1861 lbl.selectable = true
1863 if options[0] && options[0][:topwidget]
1864 dialog.vbox.add(options[0][:topwidget])
1866 if options[0] && options[0][:scrolled]
1867 sw = Gtk::ScrolledWindow.new(nil, nil)
1868 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1869 sw.add_with_viewport(lbl)
1871 dialog.set_default_size(500, 600)
1873 dialog.vbox.add(lbl)
1874 dialog.set_default_size(200, 120)
1876 if options[0] && options[0][:okcancel]
1877 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1879 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1881 if options[0] && options[0][:pos_centered]
1882 dialog.window_position = Gtk::Window::POS_CENTER
1884 dialog.window_position = Gtk::Window::POS_MOUSE
1887 if options[0] && options[0][:linkurl]
1888 linkbut = Gtk::Button.new('')
1889 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1890 linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1891 linkbut.relief = Gtk::RELIEF_NONE
1892 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1893 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1894 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1899 if !options[0] || !options[0][:not_transient]
1900 dialog.transient_for = parent
1901 dialog.run { |response|
1903 if options[0] && options[0][:okcancel]
1904 return response == Gtk::Dialog::RESPONSE_OK
1908 dialog.signal_connect('response') { dialog.destroy }
1912 def backend_wait_message(parent, msg, infopipe_path, mode)
1914 w.set_transient_for(parent)
1917 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1918 vb.pack_start(Gtk::Label.new(msg), false, false)
1920 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1921 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1922 if mode != 'one dir scan'
1923 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1925 if mode == 'web-album'
1926 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1927 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1929 vb.pack_start(Gtk::HSeparator.new, false, false)
1931 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1932 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1933 vb.pack_end(bottom, false, false)
1935 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1936 refresh_thread = Thread.new {
1937 directories_counter = 0
1938 while line = infopipe.gets
1939 if line =~ /^directories: (\d+), sizes: (\d+)/
1940 directories = $1.to_f + 1
1942 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1943 elements = $3.to_f + 1
1944 if mode == 'web-album'
1948 gtk_thread_protect { pb1_1.fraction = 0 }
1949 if mode != 'one dir scan'
1950 newtext = utf8(full_src_dir_to_rel($1, $2))
1951 newtext = '/' if newtext == ''
1952 gtk_thread_protect { pb1_2.text = newtext }
1953 directories_counter += 1
1954 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1956 elsif line =~ /^processing element$/
1957 element_counter += 1
1958 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1959 elsif line =~ /^processing size$/
1960 element_counter += 1
1961 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1962 elsif line =~ /^finished processing sizes$/
1963 gtk_thread_protect { pb1_1.fraction = 1 }
1964 elsif line =~ /^creating index.html$/
1965 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1966 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1967 directories_counter = 0
1968 elsif line =~ /^index.html: (.+)\|(.+)/
1969 newtext = utf8(full_src_dir_to_rel($1, $2))
1970 newtext = '/' if newtext == ''
1971 gtk_thread_protect { pb2.text = newtext }
1972 directories_counter += 1
1973 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1974 elsif line =~ /^die: (.*)$/
1981 w.signal_connect('delete-event') { w.destroy }
1982 w.signal_connect('destroy') {
1983 Thread.kill(refresh_thread)
1984 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1987 system("rm -f #{infopipe_path}")
1990 w.window_position = Gtk::Window::POS_CENTER
1996 def call_backend(cmd, waitmsg, mode, params)
1997 pipe = Tempfile.new("boohpipe")
1999 system("mkfifo #{pipe.path}")
2000 cmd += " --info-pipe #{pipe.path}"
2001 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2006 id, exitstatus = Process.waitpid2(pid)
2007 gtk_thread_protect { w8.destroy }
2009 if params[:successmsg]
2010 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2012 if params[:closure_after]
2013 gtk_thread_protect(¶ms[:closure_after])
2015 elsif exitstatus == 15
2016 #- say nothing, user aborted
2018 gtk_thread_protect { show_popup($main_window,
2019 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2025 button.signal_connect('clicked') {
2026 Process.kill('SIGTERM', pid)
2030 def save_changes(*forced)
2031 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2035 $xmldir.delete_attribute('already-generated')
2037 propagate_children = proc { |xmldir|
2038 if xmldir.attributes['subdirs-caption']
2039 xmldir.delete_attribute('already-generated')
2041 xmldir.elements.each('dir') { |element|
2042 propagate_children.call(element)
2046 if $xmldir.child_byname_notattr('dir', 'deleted')
2047 new_title = $subalbums_title.buffer.text
2048 if new_title != $xmldir.attributes['subdirs-caption']
2049 parent = $xmldir.parent
2050 if parent.name == 'dir'
2051 parent.delete_attribute('already-generated')
2053 propagate_children.call($xmldir)
2055 $xmldir.add_attribute('subdirs-caption', new_title)
2056 $xmldir.elements.each('dir') { |element|
2057 if !element.attributes['deleted']
2058 path = element.attributes['path']
2059 newtext = $subalbums_edits[path][:editzone].buffer.text
2060 if element.attributes['subdirs-caption']
2061 if element.attributes['subdirs-caption'] != newtext
2062 propagate_children.call(element)
2064 element.add_attribute('subdirs-caption', newtext)
2065 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2067 if element.attributes['thumbnails-caption'] != newtext
2068 element.delete_attribute('already-generated')
2070 element.add_attribute('thumbnails-caption', newtext)
2071 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2077 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2078 if $xmldir.attributes['thumbnails-caption']
2079 path = $xmldir.attributes['path']
2080 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2082 elsif $xmldir.attributes['thumbnails-caption']
2083 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2086 #- remove and reinsert elements to reflect new ordering
2089 $xmldir.elements.each { |element|
2090 if element.name == 'image' || element.name == 'video'
2091 saves[element.attributes['filename']] = element.remove
2095 $autotable.current_order.each { |path|
2096 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2097 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2100 saves.each_key { |path|
2101 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2102 chld.add_attribute('deleted', 'true')
2106 def sort_by_exif_date
2110 $xmldir.elements.each { |element|
2111 if element.name == 'image' || element.name == 'video'
2112 current_order << element.attributes['filename']
2116 #- look for EXIF dates
2118 w.set_transient_for($main_window)
2120 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2121 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2122 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2123 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2124 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2125 vb.pack_end(bottom, false, false)
2127 w.signal_connect('delete-event') { w.destroy }
2128 w.window_position = Gtk::Window::POS_CENTER
2132 b.signal_connect('clicked') { aborted = true }
2135 current_order.each { |f|
2137 if entry2type(f) == 'image'
2139 pb.fraction = i.to_f / current_order.size
2140 Gtk.main_iteration while Gtk.events_pending?
2141 date_time = `identify -format "%[EXIF:DateTime]" '#{from_utf8($current_path + "/" + f)}'`.chomp
2142 if $? == 0 && date_time != ''
2143 dates[f] = date_time
2156 $xmldir.elements.each { |element|
2157 if element.name == 'image' || element.name == 'video'
2158 saves[element.attributes['filename']] = element.remove
2162 #- find a good fallback for all entries without a date (still next to the item they were next to)
2163 neworder = dates.keys.sort { |a,b| dates[a] <=> dates[b] }
2164 for i in 0 .. current_order.size - 1
2165 if ! neworder.include?(current_order[i])
2167 while j > 0 && ! neworder.include?(current_order[j])
2170 neworder[(neworder.index(current_order[j]) || -1 ) + 1, 0] = current_order[i]
2174 $xmldir.add_element(saves[f].name, saves[f].attributes)
2177 #- let the auto-table reflect new ordering
2181 def remove_all_captions
2184 $autotable.current_order.each { |path|
2185 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2186 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2188 save_undo(_("remove all captions"),
2190 texts.each_key { |key|
2191 $name2widgets[key][:textview].buffer.text = texts[key]
2193 $notebook.set_page(1)
2195 texts.each_key { |key|
2196 $name2widgets[key][:textview].buffer.text = ''
2198 $notebook.set_page(1)
2204 $selected_elements.each_key { |path|
2205 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2211 $selected_elements = {}
2215 $undo_tb.sensitive = $undo_mb.sensitive = false
2216 $redo_tb.sensitive = $redo_mb.sensitive = false
2222 $subalbums_vb.children.each { |chld|
2223 $subalbums_vb.remove(chld)
2225 $subalbums = Gtk::Table.new(0, 0, true)
2226 current_y_sub_albums = 0
2228 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2229 $subalbums_edits = {}
2230 subalbums_counter = 0
2231 subalbums_edits_bypos = {}
2233 add_subalbum = proc { |xmldir, counter|
2234 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2235 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2236 if xmldir == $xmldir
2237 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2238 caption = xmldir.attributes['thumbnails-caption']
2239 captionfile, dummy = find_subalbum_caption_info(xmldir)
2240 infotype = 'thumbnails'
2242 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2243 captionfile, caption = find_subalbum_caption_info(xmldir)
2244 infotype = find_subalbum_info_type(xmldir)
2246 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2247 hbox = Gtk::HBox.new
2248 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2250 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2253 my_gen_real_thumbnail = proc {
2254 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2257 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2258 f.add(img = Gtk::Image.new)
2259 my_gen_real_thumbnail.call
2261 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2263 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2264 $subalbums.attach(hbox,
2265 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2267 frame, textview = create_editzone($subalbums_sw, 0, img)
2268 textview.buffer.text = caption
2269 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2270 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2272 change_image = proc {
2273 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2275 Gtk::FileChooser::ACTION_OPEN,
2277 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2278 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2279 fc.transient_for = $main_window
2280 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))
2281 f.add(preview_img = Gtk::Image.new)
2283 fc.signal_connect('update-preview') { |w|
2285 if fc.preview_filename
2286 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2287 fc.preview_widget_active = true
2289 rescue Gdk::PixbufError
2290 fc.preview_widget_active = false
2293 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2295 old_file = captionfile
2296 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2297 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2298 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2299 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2301 new_file = fc.filename
2302 msg 3, "new captionfile is: #{fc.filename}"
2303 perform_changefile = proc {
2304 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2305 $modified_pixbufs.delete(thumbnail_file)
2306 xmldir.delete_attribute("#{infotype}-rotate")
2307 xmldir.delete_attribute("#{infotype}-color-swap")
2308 xmldir.delete_attribute("#{infotype}-enhance")
2309 xmldir.delete_attribute("#{infotype}-frame-offset")
2310 my_gen_real_thumbnail.call
2312 perform_changefile.call
2314 save_undo(_("change caption file for sub-album"),
2316 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2317 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2318 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2319 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2320 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2321 my_gen_real_thumbnail.call
2322 $notebook.set_page(0)
2324 perform_changefile.call
2325 $notebook.set_page(0)
2333 system("rm -f '#{thumbnail_file}'")
2334 my_gen_real_thumbnail.call
2337 rotate_and_cleanup = proc { |angle|
2338 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2339 system("rm -f '#{thumbnail_file}'")
2342 move = proc { |direction|
2345 save_changes('forced')
2346 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2347 if direction == 'up'
2348 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2349 subalbums_edits_bypos[oldpos - 1][:position] += 1
2351 if direction == 'down'
2352 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2353 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2355 if direction == 'top'
2356 for i in 1 .. oldpos - 1
2357 subalbums_edits_bypos[i][:position] += 1
2359 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2361 if direction == 'bottom'
2362 for i in oldpos + 1 .. subalbums_counter
2363 subalbums_edits_bypos[i][:position] -= 1
2365 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2369 $xmldir.elements.each('dir') { |element|
2370 if (!element.attributes['deleted'])
2371 elems << [ element.attributes['path'], element.remove ]
2374 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2375 each { |e| $xmldir.add_element(e[1]) }
2376 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2377 $xmldir.elements.each('descendant::dir') { |elem|
2378 elem.delete_attribute('already-generated')
2381 sel = $albums_tv.selection.selected_rows
2383 populate_subalbums_treeview(false)
2384 $albums_tv.selection.select_path(sel[0])
2387 color_swap_and_cleanup = proc {
2388 perform_color_swap_and_cleanup = proc {
2389 color_swap(xmldir, "#{infotype}-")
2390 my_gen_real_thumbnail.call
2392 perform_color_swap_and_cleanup.call
2394 save_undo(_("color swap"),
2396 perform_color_swap_and_cleanup.call
2397 $notebook.set_page(0)
2399 perform_color_swap_and_cleanup.call
2400 $notebook.set_page(0)
2405 change_frame_offset_and_cleanup = proc {
2406 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2407 perform_change_frame_offset_and_cleanup = proc { |val|
2408 change_frame_offset(xmldir, "#{infotype}-", val)
2409 my_gen_real_thumbnail.call
2411 perform_change_frame_offset_and_cleanup.call(values[:new])
2413 save_undo(_("specify frame offset"),
2415 perform_change_frame_offset_and_cleanup.call(values[:old])
2416 $notebook.set_page(0)
2418 perform_change_frame_offset_and_cleanup.call(values[:new])
2419 $notebook.set_page(0)
2425 whitebalance_and_cleanup = proc {
2426 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2427 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2428 perform_change_whitebalance_and_cleanup = proc { |val|
2429 change_whitebalance(xmldir, "#{infotype}-", val)
2430 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2431 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2432 system("rm -f '#{thumbnail_file}'")
2434 perform_change_whitebalance_and_cleanup.call(values[:new])
2436 save_undo(_("fix white balance"),
2438 perform_change_whitebalance_and_cleanup.call(values[:old])
2439 $notebook.set_page(0)
2441 perform_change_whitebalance_and_cleanup.call(values[:new])
2442 $notebook.set_page(0)
2448 gammacorrect_and_cleanup = proc {
2449 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2450 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2451 perform_change_gammacorrect_and_cleanup = proc { |val|
2452 change_gammacorrect(xmldir, "#{infotype}-", val)
2453 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2454 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2455 system("rm -f '#{thumbnail_file}'")
2457 perform_change_gammacorrect_and_cleanup.call(values[:new])
2459 save_undo(_("gamma correction"),
2461 perform_change_gammacorrect_and_cleanup.call(values[:old])
2462 $notebook.set_page(0)
2464 perform_change_gammacorrect_and_cleanup.call(values[:new])
2465 $notebook.set_page(0)
2471 enhance_and_cleanup = proc {
2472 perform_enhance_and_cleanup = proc {
2473 enhance(xmldir, "#{infotype}-")
2474 my_gen_real_thumbnail.call
2477 perform_enhance_and_cleanup.call
2479 save_undo(_("enhance"),
2481 perform_enhance_and_cleanup.call
2482 $notebook.set_page(0)
2484 perform_enhance_and_cleanup.call
2485 $notebook.set_page(0)
2490 evtbox.signal_connect('button-press-event') { |w, event|
2491 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2493 rotate_and_cleanup.call(90)
2495 rotate_and_cleanup.call(-90)
2496 elsif $enhance.active?
2497 enhance_and_cleanup.call
2500 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2501 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2502 { :forbid_left => true, :forbid_right => true,
2503 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2504 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2505 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2506 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2507 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2509 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2514 evtbox.signal_connect('button-press-event') { |w, event|
2515 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2519 evtbox.signal_connect('button-release-event') { |w, event|
2520 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2521 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2522 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2523 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2524 msg 3, "gesture rotate: #{angle}"
2525 rotate_and_cleanup.call(angle)
2528 $gesture_press = nil
2531 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2532 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2533 current_y_sub_albums += 1
2536 if $xmldir.child_byname_notattr('dir', 'deleted')
2538 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2539 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2540 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2541 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2542 #- this album image/caption
2543 if $xmldir.attributes['thumbnails-caption']
2544 add_subalbum.call($xmldir, 0)
2547 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2548 $xmldir.elements.each { |element|
2549 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2550 #- element (image or video) of this album
2551 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2552 msg 3, "dest_img: #{dest_img}"
2553 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2554 total[element.name] += 1
2556 if element.name == 'dir' && !element.attributes['deleted']
2557 #- sub-album image/caption
2558 add_subalbum.call(element, subalbums_counter += 1)
2559 total[element.name] += 1
2562 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2563 total['image'], total['video'], total['dir'] ]))
2564 $subalbums_vb.add($subalbums)
2565 $subalbums_vb.show_all
2567 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2568 $notebook.get_tab_label($autotable_sw).sensitive = false
2569 $notebook.set_page(0)
2570 $thumbnails_title.buffer.text = ''
2572 $notebook.get_tab_label($autotable_sw).sensitive = true
2573 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2576 if !$xmldir.child_byname_notattr('dir', 'deleted')
2577 $notebook.get_tab_label($subalbums_sw).sensitive = false
2578 $notebook.set_page(1)
2580 $notebook.get_tab_label($subalbums_sw).sensitive = true
2584 def pixbuf_or_nil(filename)
2586 return Gdk::Pixbuf.new(filename)
2592 def theme_choose(current)
2593 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2595 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2596 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2597 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2599 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2600 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2601 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2602 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2603 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2604 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2605 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2606 treeview.signal_connect('button-press-event') { |w, event|
2607 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2608 dialog.response(Gtk::Dialog::RESPONSE_OK)
2612 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2614 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2617 iter[0] = File.basename(dir)
2618 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2619 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2620 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2621 if File.basename(dir) == current
2622 treeview.selection.select_iter(iter)
2626 dialog.set_default_size(700, 400)
2627 dialog.vbox.show_all
2628 dialog.run { |response|
2629 iter = treeview.selection.selected
2631 if response == Gtk::Dialog::RESPONSE_OK && iter
2632 return model.get_value(iter, 0)
2638 def show_password_protections
2639 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2640 child_iter = $albums_iters[xmldir.attributes['path']]
2641 if xmldir.attributes['password-protect']
2642 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2643 already_protected = true
2644 elsif already_protected
2645 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2647 pix = pix.saturate_and_pixelate(1, true)
2653 xmldir.elements.each('dir') { |elem|
2654 if !elem.attributes['deleted']
2655 examine_dir_elem.call(child_iter, elem, already_protected)
2659 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2662 def populate_subalbums_treeview(select_first)
2666 $subalbums_vb.children.each { |chld|
2667 $subalbums_vb.remove(chld)
2670 source = $xmldoc.root.attributes['source']
2671 msg 3, "source: #{source}"
2673 xmldir = $xmldoc.elements['//dir']
2674 if !xmldir || xmldir.attributes['path'] != source
2675 msg 1, _("Corrupted booh file...")
2679 append_dir_elem = proc { |parent_iter, xmldir|
2680 child_iter = $albums_ts.append(parent_iter)
2681 child_iter[0] = File.basename(xmldir.attributes['path'])
2682 child_iter[1] = xmldir.attributes['path']
2683 $albums_iters[xmldir.attributes['path']] = child_iter
2684 msg 3, "puttin location: #{xmldir.attributes['path']}"
2685 xmldir.elements.each('dir') { |elem|
2686 if !elem.attributes['deleted']
2687 append_dir_elem.call(child_iter, elem)
2691 append_dir_elem.call(nil, xmldir)
2692 show_password_protections
2694 $albums_tv.expand_all
2696 $albums_tv.selection.select_iter($albums_ts.iter_first)
2700 def open_file(filename)
2704 $current_path = nil #- invalidate
2705 $modified_pixbufs = {}
2708 $subalbums_vb.children.each { |chld|
2709 $subalbums_vb.remove(chld)
2712 if !File.exists?(filename)
2713 return utf8(_("File not found."))
2717 $xmldoc = REXML::Document.new File.new(filename)
2722 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2723 if entry2type(filename).nil?
2724 return utf8(_("Not a booh file!"))
2726 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."))
2730 if !source = $xmldoc.root.attributes['source']
2731 return utf8(_("Corrupted booh file..."))
2734 if !dest = $xmldoc.root.attributes['destination']
2735 return utf8(_("Corrupted booh file..."))
2738 if !theme = $xmldoc.root.attributes['theme']
2739 return utf8(_("Corrupted booh file..."))
2742 if $xmldoc.root.attributes['version'] < '0.8.4'
2743 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2744 mark_document_as_dirty
2745 if $xmldoc.root.attributes['version'] < '0.8.4'
2746 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2747 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2748 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2749 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2750 if old_dest_dir != new_dest_dir
2751 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2753 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2754 xmldir.elements.each { |element|
2755 if %w(image video).include?(element.name) && !element.attributes['deleted']
2756 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2757 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2758 Dir[old_name + '*'].each { |file|
2759 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2760 file != new_file and sys("mv '#{file}' '#{new_file}'")
2763 if element.name == 'dir' && !element.attributes['deleted']
2764 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2765 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2766 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2770 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2774 $xmldoc.root.add_attribute('version', $VERSION)
2777 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2778 optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2779 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2781 $filename = filename
2782 select_theme(theme, limit_sizes, optimizefor32, nperrow)
2783 $default_size['thumbnails'] =~ /(.*)x(.*)/
2784 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2785 $albums_thumbnail_size =~ /(.*)x(.*)/
2786 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2788 populate_subalbums_treeview(true)
2790 $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
2794 def open_file_user(filename)
2795 result = open_file(filename)
2797 $config['last-opens'] ||= []
2798 if $config['last-opens'][-1] != utf8(filename)
2799 $config['last-opens'] << utf8(filename)
2801 $orig_filename = $filename
2802 tmp = Tempfile.new("boohtemp")
2805 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2807 $tempfiles << $filename << "#{$filename}.backup"
2809 $orig_filename = nil
2815 if !ask_save_modifications(utf8(_("Save this album?")),
2816 utf8(_("Do you want to save the changes to this album?")),
2817 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2820 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2822 Gtk::FileChooser::ACTION_OPEN,
2824 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2825 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2826 fc.set_current_folder(File.expand_path("~/.booh"))
2827 fc.transient_for = $main_window
2830 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2831 push_mousecursor_wait(fc)
2832 msg = open_file_user(fc.filename)
2848 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2853 def additional_booh_options
2856 options += "--mproc #{$config['mproc'].to_i} "
2858 options += "--comments-format '#{$config['comments-format']}'"
2863 if !ask_save_modifications(utf8(_("Save this album?")),
2864 utf8(_("Do you want to save the changes to this album?")),
2865 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2868 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2870 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2871 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2872 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2874 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2875 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2876 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2877 tbl.attach(src = Gtk::Entry.new,
2878 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2879 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2880 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2881 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2882 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2883 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2884 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2885 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2886 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2887 tbl.attach(dest = Gtk::Entry.new,
2888 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2889 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2890 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2891 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2892 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2893 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2894 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2895 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2896 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2898 tooltips = Gtk::Tooltips.new
2899 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2900 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2901 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2902 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2903 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2904 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2905 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)
2906 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2907 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2908 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
2909 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
2910 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)
2911 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2912 pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
2913 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)
2915 src_nb_calculated_for = ''
2917 process_src_nb = proc {
2918 if src.text != src_nb_calculated_for
2919 src_nb_calculated_for = src.text
2921 Thread.kill(src_nb_thread)
2924 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
2925 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
2927 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2928 if File.readable?(from_utf8_safe(src_nb_calculated_for))
2929 src_nb_thread = Thread.new {
2930 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2931 total = { 'image' => 0, 'video' => 0, nil => 0 }
2932 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2933 if File.basename(dir) =~ /^\./
2937 Dir.entries(dir.chomp).each { |file|
2938 total[entry2type(file)] += 1
2940 rescue Errno::EACCES, Errno::ENOENT
2944 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2948 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2951 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2957 timeout_src_nb = Gtk.timeout_add(100) {
2961 src_browse.signal_connect('clicked') {
2962 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2964 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2966 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2967 fc.transient_for = $main_window
2968 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2969 src.text = utf8(fc.filename)
2971 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2976 dest_browse.signal_connect('clicked') {
2977 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2979 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2981 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2982 fc.transient_for = $main_window
2983 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2984 dest.text = utf8(fc.filename)
2989 conf_browse.signal_connect('clicked') {
2990 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2992 Gtk::FileChooser::ACTION_SAVE,
2994 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2995 fc.transient_for = $main_window
2996 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2997 fc.set_current_folder(File.expand_path("~/.booh"))
2998 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2999 conf.text = utf8(fc.filename)
3006 recreate_theme_config = proc {
3007 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3009 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3010 $images_size.each { |s|
3011 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3015 tooltips.set_tip(cb, utf8(s['description']), nil)
3016 theme_sizes << { :widget => cb, :value => s['name'] }
3018 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3019 tooltips = Gtk::Tooltips.new
3020 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3021 theme_sizes << { :widget => cb, :value => 'original' }
3024 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3027 $allowed_N_values.each { |n|
3029 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3031 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3033 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3037 nperrows << { :widget => rb, :value => n }
3039 nperrowradios.show_all
3041 recreate_theme_config.call
3043 theme_button.signal_connect('clicked') {
3044 if newtheme = theme_choose(theme_button.label)
3045 theme_button.label = newtheme
3046 recreate_theme_config.call
3050 dialog.vbox.add(frame1)
3051 dialog.vbox.add(frame2)
3052 dialog.window_position = Gtk::Window::POS_MOUSE
3058 dialog.run { |response|
3059 if response == Gtk::Dialog::RESPONSE_OK
3060 srcdir = from_utf8_safe(src.text)
3061 destdir = from_utf8_safe(dest.text)
3062 confpath = from_utf8_safe(conf.text)
3063 if src.text != '' && srcdir == ''
3064 show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3066 elsif !File.directory?(srcdir)
3067 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3069 elsif dest.text != '' && destdir == ''
3070 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3072 elsif destdir != make_dest_filename(destdir)
3073 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3075 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3076 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
3078 elsif File.exists?(destdir) && !File.directory?(destdir)
3079 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3081 elsif conf.text == ''
3082 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3084 elsif conf.text != '' && confpath == ''
3085 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3087 elsif File.directory?(confpath)
3088 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3090 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3091 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3093 system("mkdir '#{destdir}'")
3094 if !File.directory?(destdir)
3095 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3107 srcdir = from_utf8(src.text)
3108 destdir = from_utf8(dest.text)
3109 configskel = File.expand_path(from_utf8(conf.text))
3110 theme = theme_button.label
3111 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3112 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3113 opt432 = optimize432.active?
3114 madewith = madewithentry.text
3115 indexlink = indexlinkentry.text
3118 Thread.kill(src_nb_thread)
3119 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3122 Gtk.timeout_remove(timeout_src_nb)
3125 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3126 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3127 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3128 utf8(_("Please wait while scanning source directory...")),
3130 { :closure_after => proc { open_file_user(configskel) } })
3135 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3137 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3138 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3139 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3141 source = $xmldoc.root.attributes['source']
3142 dest = $xmldoc.root.attributes['destination']
3143 theme = $xmldoc.root.attributes['theme']
3144 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3145 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3146 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3148 limit_sizes = limit_sizes.split(/,/)
3150 madewith = $xmldoc.root.attributes['made-with']
3151 indexlink = $xmldoc.root.attributes['index-link']
3153 tooltips = Gtk::Tooltips.new
3154 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3155 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3156 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3157 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3158 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3159 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3160 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3161 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3162 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3163 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3164 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3165 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3166 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3168 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3169 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3170 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3171 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3172 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3173 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3174 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)
3175 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3176 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3178 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3179 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3181 indexlinkentry.text = indexlink
3183 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)
3184 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3185 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3187 madewithentry.text = madewith
3189 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)
3193 recreate_theme_config = proc {
3194 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3196 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3198 $images_size.each { |s|
3199 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3201 if limit_sizes.include?(s['name'])
3209 tooltips.set_tip(cb, utf8(s['description']), nil)
3210 theme_sizes << { :widget => cb, :value => s['name'] }
3212 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3213 tooltips = Gtk::Tooltips.new
3214 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3215 if limit_sizes && limit_sizes.include?('original')
3218 theme_sizes << { :widget => cb, :value => 'original' }
3221 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3224 $allowed_N_values.each { |n|
3226 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3228 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3230 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3231 nperrowradios.add(Gtk::Label.new(' '))
3232 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3235 nperrows << { :widget => rb, :value => n.to_s }
3237 nperrowradios.show_all
3239 recreate_theme_config.call
3241 theme_button.signal_connect('clicked') {
3242 if newtheme = theme_choose(theme_button.label)
3245 theme_button.label = newtheme
3246 recreate_theme_config.call
3250 dialog.vbox.add(frame1)
3251 dialog.vbox.add(frame2)
3252 dialog.window_position = Gtk::Window::POS_MOUSE
3258 dialog.run { |response|
3259 if response == Gtk::Dialog::RESPONSE_OK
3260 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3261 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3270 save_theme = theme_button.label
3271 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3272 save_opt432 = optimize432.active?
3273 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3274 save_madewith = madewithentry.text
3275 save_indexlink = indexlinkentry.text
3278 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith || save_indexlink != indexlinkentry)
3279 mark_document_as_dirty
3281 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3282 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3283 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3284 utf8(_("Please wait while scanning source directory...")),
3286 { :closure_after => proc {
3287 open_file($filename)
3296 sel = $albums_tv.selection.selected_rows
3298 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3299 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3300 utf8(_("Please wait while scanning source directory...")),
3302 { :closure_after => proc {
3303 open_file($filename)
3304 $albums_tv.selection.select_path(sel[0])
3312 sel = $albums_tv.selection.selected_rows
3314 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3315 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3316 utf8(_("Please wait while scanning source directory...")),
3318 { :closure_after => proc {
3319 open_file($filename)
3320 $albums_tv.selection.select_path(sel[0])
3328 theme = $xmldoc.root.attributes['theme']
3329 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3331 limit_sizes = "--sizes #{limit_sizes}"
3333 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3334 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3335 utf8(_("Please wait while scanning source directory...")),
3337 { :closure_after => proc {
3338 open_file($filename)
3344 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3346 Gtk::FileChooser::ACTION_SAVE,
3348 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3349 fc.transient_for = $main_window
3350 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3351 fc.set_current_folder(File.expand_path("~/.booh"))
3352 fc.filename = $orig_filename
3353 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3354 $orig_filename = fc.filename
3355 if ! save_current_file_user
3359 $config['last-opens'] ||= []
3360 $config['last-opens'] << $orig_filename
3366 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3368 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3369 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3370 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3372 dialog.vbox.add(notebook = Gtk::Notebook.new)
3373 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3374 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3375 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3376 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)),
3377 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3378 tooltips = Gtk::Tooltips.new
3379 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3380 for example: /usr/bin/mplayer %f")), nil)
3381 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3382 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3383 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3384 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3385 tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3386 for example: /usr/bin/gimp-remote %f")), nil)
3387 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3388 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3389 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3390 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3391 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3392 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3393 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3394 0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3395 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)),
3396 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3397 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)
3398 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3399 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3400 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)
3401 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3402 0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3403 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)
3405 smp_check.signal_connect('toggled') {
3406 if smp_check.active?
3407 smp_hbox.sensitive = true
3409 smp_hbox.sensitive = false
3413 smp_check.active = true
3414 smp_spin.value = $config['mproc'].to_i
3416 nogestures_check.active = $config['nogestures']
3417 deleteondisk_check.active = $config['deleteondisk']
3419 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3420 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3421 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3422 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3423 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3424 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3425 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3426 tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3427 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3428 tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3429 2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3430 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)
3431 commentsformat_help.signal_connect('clicked') {
3432 show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3433 hence you should look at ImageMagick/identify documentation for the most
3434 accurate and up-to-date documentation. Last time I checked, documentation
3437 Print information about the image in a format of your choosing. You can
3438 include the image filename, type, width, height, Exif data, or other image
3439 attributes by embedding special format characters:
3442 %P page width and height
3446 %e filename extension
3451 %k number of unique colors
3458 %r image class and colorspace
3461 %u unique temporary filename