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 if $xmldir.attributes['thumbnails-caption']
2087 if edit = $subalbums_edits[$xmldir.attributes['path']]
2088 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2092 #- remove and reinsert elements to reflect new ordering
2095 $xmldir.elements.each { |element|
2096 if element.name == 'image' || element.name == 'video'
2097 saves[element.attributes['filename']] = element.remove
2101 $autotable.current_order.each { |path|
2102 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2103 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2106 saves.each_key { |path|
2107 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2108 chld.add_attribute('deleted', 'true')
2112 def sort_by_exif_date
2116 $xmldir.elements.each { |element|
2117 if element.name == 'image' || element.name == 'video'
2118 current_order << element.attributes['filename']
2122 #- look for EXIF dates
2124 w.set_transient_for($main_window)
2126 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2127 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2128 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2129 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2130 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2131 vb.pack_end(bottom, false, false)
2133 w.signal_connect('delete-event') { w.destroy }
2134 w.window_position = Gtk::Window::POS_CENTER
2138 b.signal_connect('clicked') { aborted = true }
2141 current_order.each { |f|
2143 if entry2type(f) == 'image'
2145 pb.fraction = i.to_f / current_order.size
2146 Gtk.main_iteration while Gtk.events_pending?
2147 date_time = `identify -format "%[EXIF:DateTime]" '#{from_utf8($current_path + "/" + f)}'`.chomp
2148 if $? == 0 && date_time != ''
2149 dates[f] = date_time
2162 $xmldir.elements.each { |element|
2163 if element.name == 'image' || element.name == 'video'
2164 saves[element.attributes['filename']] = element.remove
2168 #- find a good fallback for all entries without a date (still next to the item they were next to)
2169 neworder = dates.keys.sort { |a,b| dates[a] <=> dates[b] }
2170 for i in 0 .. current_order.size - 1
2171 if ! neworder.include?(current_order[i])
2173 while j > 0 && ! neworder.include?(current_order[j])
2176 neworder[(neworder.index(current_order[j]) || -1 ) + 1, 0] = current_order[i]
2180 $xmldir.add_element(saves[f].name, saves[f].attributes)
2183 #- let the auto-table reflect new ordering
2187 def remove_all_captions
2190 $autotable.current_order.each { |path|
2191 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2192 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2194 save_undo(_("remove all captions"),
2196 texts.each_key { |key|
2197 $name2widgets[key][:textview].buffer.text = texts[key]
2199 $notebook.set_page(1)
2201 texts.each_key { |key|
2202 $name2widgets[key][:textview].buffer.text = ''
2204 $notebook.set_page(1)
2210 $selected_elements.each_key { |path|
2211 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2217 $selected_elements = {}
2221 $undo_tb.sensitive = $undo_mb.sensitive = false
2222 $redo_tb.sensitive = $redo_mb.sensitive = false
2228 $subalbums_vb.children.each { |chld|
2229 $subalbums_vb.remove(chld)
2231 $subalbums = Gtk::Table.new(0, 0, true)
2232 current_y_sub_albums = 0
2234 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2235 $subalbums_edits = {}
2236 subalbums_counter = 0
2237 subalbums_edits_bypos = {}
2239 add_subalbum = proc { |xmldir, counter|
2240 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2241 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2242 if xmldir == $xmldir
2243 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2244 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2245 caption = xmldir.attributes['thumbnails-caption']
2246 infotype = 'thumbnails'
2248 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2249 captionfile, caption = find_subalbum_caption_info(xmldir)
2250 infotype = find_subalbum_info_type(xmldir)
2252 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2253 hbox = Gtk::HBox.new
2254 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2256 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2259 my_gen_real_thumbnail = proc {
2260 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2263 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2264 f.add(img = Gtk::Image.new)
2265 my_gen_real_thumbnail.call
2267 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2269 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2270 $subalbums.attach(hbox,
2271 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2273 frame, textview = create_editzone($subalbums_sw, 0, img)
2274 textview.buffer.text = caption
2275 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2276 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2278 change_image = proc {
2279 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2281 Gtk::FileChooser::ACTION_OPEN,
2283 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2284 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2285 fc.transient_for = $main_window
2286 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))
2287 f.add(preview_img = Gtk::Image.new)
2289 fc.signal_connect('update-preview') { |w|
2291 if fc.preview_filename
2292 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2293 fc.preview_widget_active = true
2295 rescue Gdk::PixbufError
2296 fc.preview_widget_active = false
2299 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2301 old_file = captionfile
2302 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2303 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2304 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2305 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2307 new_file = fc.filename
2308 msg 3, "new captionfile is: #{fc.filename}"
2309 perform_changefile = proc {
2310 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2311 $modified_pixbufs.delete(thumbnail_file)
2312 xmldir.delete_attribute("#{infotype}-rotate")
2313 xmldir.delete_attribute("#{infotype}-color-swap")
2314 xmldir.delete_attribute("#{infotype}-enhance")
2315 xmldir.delete_attribute("#{infotype}-frame-offset")
2316 my_gen_real_thumbnail.call
2318 perform_changefile.call
2320 save_undo(_("change caption file for sub-album"),
2322 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2323 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2324 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2325 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2326 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2327 my_gen_real_thumbnail.call
2328 $notebook.set_page(0)
2330 perform_changefile.call
2331 $notebook.set_page(0)
2339 system("rm -f '#{thumbnail_file}'")
2340 my_gen_real_thumbnail.call
2343 rotate_and_cleanup = proc { |angle|
2344 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2345 system("rm -f '#{thumbnail_file}'")
2348 move = proc { |direction|
2351 save_changes('forced')
2352 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2353 if direction == 'up'
2354 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2355 subalbums_edits_bypos[oldpos - 1][:position] += 1
2357 if direction == 'down'
2358 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2359 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2361 if direction == 'top'
2362 for i in 1 .. oldpos - 1
2363 subalbums_edits_bypos[i][:position] += 1
2365 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2367 if direction == 'bottom'
2368 for i in oldpos + 1 .. subalbums_counter
2369 subalbums_edits_bypos[i][:position] -= 1
2371 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2375 $xmldir.elements.each('dir') { |element|
2376 if (!element.attributes['deleted'])
2377 elems << [ element.attributes['path'], element.remove ]
2380 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2381 each { |e| $xmldir.add_element(e[1]) }
2382 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2383 $xmldir.elements.each('descendant::dir') { |elem|
2384 elem.delete_attribute('already-generated')
2387 sel = $albums_tv.selection.selected_rows
2389 populate_subalbums_treeview(false)
2390 $albums_tv.selection.select_path(sel[0])
2393 color_swap_and_cleanup = proc {
2394 perform_color_swap_and_cleanup = proc {
2395 color_swap(xmldir, "#{infotype}-")
2396 my_gen_real_thumbnail.call
2398 perform_color_swap_and_cleanup.call
2400 save_undo(_("color swap"),
2402 perform_color_swap_and_cleanup.call
2403 $notebook.set_page(0)
2405 perform_color_swap_and_cleanup.call
2406 $notebook.set_page(0)
2411 change_frame_offset_and_cleanup = proc {
2412 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2413 perform_change_frame_offset_and_cleanup = proc { |val|
2414 change_frame_offset(xmldir, "#{infotype}-", val)
2415 my_gen_real_thumbnail.call
2417 perform_change_frame_offset_and_cleanup.call(values[:new])
2419 save_undo(_("specify frame offset"),
2421 perform_change_frame_offset_and_cleanup.call(values[:old])
2422 $notebook.set_page(0)
2424 perform_change_frame_offset_and_cleanup.call(values[:new])
2425 $notebook.set_page(0)
2431 whitebalance_and_cleanup = proc {
2432 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2433 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2434 perform_change_whitebalance_and_cleanup = proc { |val|
2435 change_whitebalance(xmldir, "#{infotype}-", val)
2436 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2437 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2438 system("rm -f '#{thumbnail_file}'")
2440 perform_change_whitebalance_and_cleanup.call(values[:new])
2442 save_undo(_("fix white balance"),
2444 perform_change_whitebalance_and_cleanup.call(values[:old])
2445 $notebook.set_page(0)
2447 perform_change_whitebalance_and_cleanup.call(values[:new])
2448 $notebook.set_page(0)
2454 gammacorrect_and_cleanup = proc {
2455 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2456 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2457 perform_change_gammacorrect_and_cleanup = proc { |val|
2458 change_gammacorrect(xmldir, "#{infotype}-", val)
2459 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2460 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2461 system("rm -f '#{thumbnail_file}'")
2463 perform_change_gammacorrect_and_cleanup.call(values[:new])
2465 save_undo(_("gamma correction"),
2467 perform_change_gammacorrect_and_cleanup.call(values[:old])
2468 $notebook.set_page(0)
2470 perform_change_gammacorrect_and_cleanup.call(values[:new])
2471 $notebook.set_page(0)
2477 enhance_and_cleanup = proc {
2478 perform_enhance_and_cleanup = proc {
2479 enhance(xmldir, "#{infotype}-")
2480 my_gen_real_thumbnail.call
2483 perform_enhance_and_cleanup.call
2485 save_undo(_("enhance"),
2487 perform_enhance_and_cleanup.call
2488 $notebook.set_page(0)
2490 perform_enhance_and_cleanup.call
2491 $notebook.set_page(0)
2496 evtbox.signal_connect('button-press-event') { |w, event|
2497 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2499 rotate_and_cleanup.call(90)
2501 rotate_and_cleanup.call(-90)
2502 elsif $enhance.active?
2503 enhance_and_cleanup.call
2506 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2507 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2508 { :forbid_left => true, :forbid_right => true,
2509 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2510 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2511 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2512 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2513 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2515 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2520 evtbox.signal_connect('button-press-event') { |w, event|
2521 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2525 evtbox.signal_connect('button-release-event') { |w, event|
2526 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2527 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2528 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2529 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2530 msg 3, "gesture rotate: #{angle}"
2531 rotate_and_cleanup.call(angle)
2534 $gesture_press = nil
2537 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2538 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2539 current_y_sub_albums += 1
2542 if $xmldir.child_byname_notattr('dir', 'deleted')
2544 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2545 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2546 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2547 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2548 #- this album image/caption
2549 if $xmldir.attributes['thumbnails-caption']
2550 add_subalbum.call($xmldir, 0)
2553 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2554 $xmldir.elements.each { |element|
2555 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2556 #- element (image or video) of this album
2557 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2558 msg 3, "dest_img: #{dest_img}"
2559 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2560 total[element.name] += 1
2562 if element.name == 'dir' && !element.attributes['deleted']
2563 #- sub-album image/caption
2564 add_subalbum.call(element, subalbums_counter += 1)
2565 total[element.name] += 1
2568 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2569 total['image'], total['video'], total['dir'] ]))
2570 $subalbums_vb.add($subalbums)
2571 $subalbums_vb.show_all
2573 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2574 $notebook.get_tab_label($autotable_sw).sensitive = false
2575 $notebook.set_page(0)
2576 $thumbnails_title.buffer.text = ''
2578 $notebook.get_tab_label($autotable_sw).sensitive = true
2579 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2582 if !$xmldir.child_byname_notattr('dir', 'deleted')
2583 $notebook.get_tab_label($subalbums_sw).sensitive = false
2584 $notebook.set_page(1)
2586 $notebook.get_tab_label($subalbums_sw).sensitive = true
2590 def pixbuf_or_nil(filename)
2592 return Gdk::Pixbuf.new(filename)
2598 def theme_choose(current)
2599 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2601 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2602 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2603 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2605 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2606 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2607 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2608 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2609 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2610 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2611 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2612 treeview.signal_connect('button-press-event') { |w, event|
2613 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2614 dialog.response(Gtk::Dialog::RESPONSE_OK)
2618 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2620 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2623 iter[0] = File.basename(dir)
2624 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2625 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2626 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2627 if File.basename(dir) == current
2628 treeview.selection.select_iter(iter)
2632 dialog.set_default_size(700, 400)
2633 dialog.vbox.show_all
2634 dialog.run { |response|
2635 iter = treeview.selection.selected
2637 if response == Gtk::Dialog::RESPONSE_OK && iter
2638 return model.get_value(iter, 0)
2644 def show_password_protections
2645 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2646 child_iter = $albums_iters[xmldir.attributes['path']]
2647 if xmldir.attributes['password-protect']
2648 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2649 already_protected = true
2650 elsif already_protected
2651 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2653 pix = pix.saturate_and_pixelate(1, true)
2659 xmldir.elements.each('dir') { |elem|
2660 if !elem.attributes['deleted']
2661 examine_dir_elem.call(child_iter, elem, already_protected)
2665 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2668 def populate_subalbums_treeview(select_first)
2672 $subalbums_vb.children.each { |chld|
2673 $subalbums_vb.remove(chld)
2676 source = $xmldoc.root.attributes['source']
2677 msg 3, "source: #{source}"
2679 xmldir = $xmldoc.elements['//dir']
2680 if !xmldir || xmldir.attributes['path'] != source
2681 msg 1, _("Corrupted booh file...")
2685 append_dir_elem = proc { |parent_iter, xmldir|
2686 child_iter = $albums_ts.append(parent_iter)
2687 child_iter[0] = File.basename(xmldir.attributes['path'])
2688 child_iter[1] = xmldir.attributes['path']
2689 $albums_iters[xmldir.attributes['path']] = child_iter
2690 msg 3, "puttin location: #{xmldir.attributes['path']}"
2691 xmldir.elements.each('dir') { |elem|
2692 if !elem.attributes['deleted']
2693 append_dir_elem.call(child_iter, elem)
2697 append_dir_elem.call(nil, xmldir)
2698 show_password_protections
2700 $albums_tv.expand_all
2702 $albums_tv.selection.select_iter($albums_ts.iter_first)
2706 def open_file(filename)
2710 $current_path = nil #- invalidate
2711 $modified_pixbufs = {}
2714 $subalbums_vb.children.each { |chld|
2715 $subalbums_vb.remove(chld)
2718 if !File.exists?(filename)
2719 return utf8(_("File not found."))
2723 $xmldoc = REXML::Document.new File.new(filename)
2728 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2729 if entry2type(filename).nil?
2730 return utf8(_("Not a booh file!"))
2732 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."))
2736 if !source = $xmldoc.root.attributes['source']
2737 return utf8(_("Corrupted booh file..."))
2740 if !dest = $xmldoc.root.attributes['destination']
2741 return utf8(_("Corrupted booh file..."))
2744 if !theme = $xmldoc.root.attributes['theme']
2745 return utf8(_("Corrupted booh file..."))
2748 if $xmldoc.root.attributes['version'] < '0.8.4'
2749 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2750 mark_document_as_dirty
2751 if $xmldoc.root.attributes['version'] < '0.8.4'
2752 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2753 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2754 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2755 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2756 if old_dest_dir != new_dest_dir
2757 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2759 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2760 xmldir.elements.each { |element|
2761 if %w(image video).include?(element.name) && !element.attributes['deleted']
2762 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2763 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2764 Dir[old_name + '*'].each { |file|
2765 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2766 file != new_file and sys("mv '#{file}' '#{new_file}'")
2769 if element.name == 'dir' && !element.attributes['deleted']
2770 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2771 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2772 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2776 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2780 $xmldoc.root.add_attribute('version', $VERSION)
2783 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2784 optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2785 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2787 $filename = filename
2788 select_theme(theme, limit_sizes, optimizefor32, nperrow)
2789 $default_size['thumbnails'] =~ /(.*)x(.*)/
2790 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2791 $albums_thumbnail_size =~ /(.*)x(.*)/
2792 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2794 populate_subalbums_treeview(true)
2796 $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
2800 def open_file_user(filename)
2801 result = open_file(filename)
2803 $config['last-opens'] ||= []
2804 if $config['last-opens'][-1] != utf8(filename)
2805 $config['last-opens'] << utf8(filename)
2807 $orig_filename = $filename
2808 tmp = Tempfile.new("boohtemp")
2811 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2813 $tempfiles << $filename << "#{$filename}.backup"
2815 $orig_filename = nil
2821 if !ask_save_modifications(utf8(_("Save this album?")),
2822 utf8(_("Do you want to save the changes to this album?")),
2823 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2826 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2828 Gtk::FileChooser::ACTION_OPEN,
2830 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2831 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2832 fc.set_current_folder(File.expand_path("~/.booh"))
2833 fc.transient_for = $main_window
2836 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2837 push_mousecursor_wait(fc)
2838 msg = open_file_user(fc.filename)
2854 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2859 def additional_booh_options
2862 options += "--mproc #{$config['mproc'].to_i} "
2864 options += "--comments-format '#{$config['comments-format']}'"
2869 if !ask_save_modifications(utf8(_("Save this album?")),
2870 utf8(_("Do you want to save the changes to this album?")),
2871 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2874 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2876 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2877 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2878 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2880 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2881 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2882 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2883 tbl.attach(src = Gtk::Entry.new,
2884 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2885 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2886 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2887 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2888 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2889 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2890 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2891 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2892 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2893 tbl.attach(dest = Gtk::Entry.new,
2894 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2895 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2896 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2897 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2898 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2899 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2900 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2901 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2902 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2904 tooltips = Gtk::Tooltips.new
2905 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2906 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2907 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2908 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2909 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2910 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2911 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)
2912 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2913 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2914 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
2915 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
2916 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)
2917 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2918 pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
2919 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)
2921 src_nb_calculated_for = ''
2923 process_src_nb = proc {
2924 if src.text != src_nb_calculated_for
2925 src_nb_calculated_for = src.text
2927 Thread.kill(src_nb_thread)
2930 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
2931 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
2933 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2934 if File.readable?(from_utf8_safe(src_nb_calculated_for))
2935 src_nb_thread = Thread.new {
2936 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2937 total = { 'image' => 0, 'video' => 0, nil => 0 }
2938 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2939 if File.basename(dir) =~ /^\./
2943 Dir.entries(dir.chomp).each { |file|
2944 total[entry2type(file)] += 1
2946 rescue Errno::EACCES, Errno::ENOENT
2950 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2954 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2957 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2963 timeout_src_nb = Gtk.timeout_add(100) {
2967 src_browse.signal_connect('clicked') {
2968 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2970 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2972 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2973 fc.transient_for = $main_window
2974 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2975 src.text = utf8(fc.filename)
2977 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2982 dest_browse.signal_connect('clicked') {
2983 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2985 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2987 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2988 fc.transient_for = $main_window
2989 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2990 dest.text = utf8(fc.filename)
2995 conf_browse.signal_connect('clicked') {
2996 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2998 Gtk::FileChooser::ACTION_SAVE,
3000 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3001 fc.transient_for = $main_window
3002 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3003 fc.set_current_folder(File.expand_path("~/.booh"))
3004 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3005 conf.text = utf8(fc.filename)
3012 recreate_theme_config = proc {
3013 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3015 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3016 $images_size.each { |s|
3017 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3021 tooltips.set_tip(cb, utf8(s['description']), nil)
3022 theme_sizes << { :widget => cb, :value => s['name'] }
3024 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3025 tooltips = Gtk::Tooltips.new
3026 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3027 theme_sizes << { :widget => cb, :value => 'original' }
3030 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3033 $allowed_N_values.each { |n|
3035 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3037 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3039 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3043 nperrows << { :widget => rb, :value => n }
3045 nperrowradios.show_all
3047 recreate_theme_config.call
3049 theme_button.signal_connect('clicked') {
3050 if newtheme = theme_choose(theme_button.label)
3051 theme_button.label = newtheme
3052 recreate_theme_config.call
3056 dialog.vbox.add(frame1)
3057 dialog.vbox.add(frame2)
3058 dialog.window_position = Gtk::Window::POS_MOUSE
3064 dialog.run { |response|
3065 if response == Gtk::Dialog::RESPONSE_OK
3066 srcdir = from_utf8_safe(src.text)
3067 destdir = from_utf8_safe(dest.text)
3068 confpath = from_utf8_safe(conf.text)
3069 if src.text != '' && srcdir == ''
3070 show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3072 elsif !File.directory?(srcdir)
3073 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3075 elsif dest.text != '' && destdir == ''
3076 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3078 elsif destdir != make_dest_filename(destdir)
3079 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3081 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3082 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
3084 elsif File.exists?(destdir) && !File.directory?(destdir)
3085 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3087 elsif conf.text == ''
3088 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3090 elsif conf.text != '' && confpath == ''
3091 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3093 elsif File.directory?(confpath)
3094 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3096 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3097 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3099 system("mkdir '#{destdir}'")
3100 if !File.directory?(destdir)
3101 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3113 srcdir = from_utf8(src.text)
3114 destdir = from_utf8(dest.text)
3115 configskel = File.expand_path(from_utf8(conf.text))
3116 theme = theme_button.label
3117 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3118 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3119 opt432 = optimize432.active?
3120 madewith = madewithentry.text
3121 indexlink = indexlinkentry.text
3124 Thread.kill(src_nb_thread)
3125 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3128 Gtk.timeout_remove(timeout_src_nb)
3131 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3132 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3133 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3134 utf8(_("Please wait while scanning source directory...")),
3136 { :closure_after => proc { open_file_user(configskel) } })
3141 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3143 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3144 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3145 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3147 source = $xmldoc.root.attributes['source']
3148 dest = $xmldoc.root.attributes['destination']
3149 theme = $xmldoc.root.attributes['theme']
3150 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3151 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3152 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3154 limit_sizes = limit_sizes.split(/,/)
3156 madewith = $xmldoc.root.attributes['made-with']
3157 indexlink = $xmldoc.root.attributes['index-link']
3159 tooltips = Gtk::Tooltips.new
3160 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3161 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3162 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3163 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3164 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3165 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3166 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3167 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3168 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3169 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3170 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3171 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3172 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3174 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3175 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3176 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3177 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3178 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3179 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3180 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)
3181 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3182 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3184 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3185 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3187 indexlinkentry.text = indexlink
3189 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)
3190 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3191 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3193 madewithentry.text = madewith
3195 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)
3199 recreate_theme_config = proc {
3200 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3202 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3204 $images_size.each { |s|
3205 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3207 if limit_sizes.include?(s['name'])
3215 tooltips.set_tip(cb, utf8(s['description']), nil)
3216 theme_sizes << { :widget => cb, :value => s['name'] }
3218 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3219 tooltips = Gtk::Tooltips.new
3220 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3221 if limit_sizes && limit_sizes.include?('original')
3224 theme_sizes << { :widget => cb, :value => 'original' }
3227 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3230 $allowed_N_values.each { |n|
3232 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3234 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3236 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3237 nperrowradios.add(Gtk::Label.new(' '))
3238 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3241 nperrows << { :widget => rb, :value => n.to_s }
3243 nperrowradios.show_all
3245 recreate_theme_config.call
3247 theme_button.signal_connect('clicked') {
3248 if newtheme = theme_choose(theme_button.label)
3251 theme_button.label = newtheme
3252 recreate_theme_config.call
3256 dialog.vbox.add(frame1)
3257 dialog.vbox.add(frame2)
3258 dialog.window_position = Gtk::Window::POS_MOUSE
3264 dialog.run { |response|
3265 if response == Gtk::Dialog::RESPONSE_OK
3266 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3267 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3276 save_theme = theme_button.label
3277 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3278 save_opt432 = optimize432.active?
3279 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3280 save_madewith = madewithentry.text
3281 save_indexlink = indexlinkentry.text
3284 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith || save_indexlink != indexlinkentry)
3285 mark_document_as_dirty
3287 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3288 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3289 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3290 utf8(_("Please wait while scanning source directory...")),
3292 { :closure_after => proc {
3293 open_file($filename)
3302 sel = $albums_tv.selection.selected_rows
3304 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3305 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3306 utf8(_("Please wait while scanning source directory...")),
3308 { :closure_after => proc {
3309 open_file($filename)
3310 $albums_tv.selection.select_path(sel[0])
3318 sel = $albums_tv.selection.selected_rows
3320 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3321 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3322 utf8(_("Please wait while scanning source directory...")),
3324 { :closure_after => proc {
3325 open_file($filename)
3326 $albums_tv.selection.select_path(sel[0])
3334 theme = $xmldoc.root.attributes['theme']
3335 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3337 limit_sizes = "--sizes #{limit_sizes}"
3339 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3340 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3341 utf8(_("Please wait while scanning source directory...")),
3343 { :closure_after => proc {
3344 open_file($filename)
3350 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3352 Gtk::FileChooser::ACTION_SAVE,
3354 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3355 fc.transient_for = $main_window
3356 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3357 fc.set_current_folder(File.expand_path("~/.booh"))
3358 fc.filename = $orig_filename
3359 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3360 $orig_filename = fc.filename
3361 if ! save_current_file_user
3365 $config['last-opens'] ||= []
3366 $config['last-opens'] << $orig_filename
3372 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3374 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3375 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3376 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3378 dialog.vbox.add(notebook = Gtk::Notebook.new)
3379 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3380 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3381 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3382 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)),
3383 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3384 tooltips = Gtk::Tooltips.new
3385 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3386 for example: /usr/bin/mplayer %f")), nil)
3387 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3388 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3389 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3390 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3391 tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3392 for example: /usr/bin/gimp-remote %f")), nil)
3393 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3394 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3395 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3396 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3397 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3398 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3399 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3400 0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3401 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)),
3402 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3403 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)
3404 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3405 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3406 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)
3407 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3408 0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3409 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)
3411 smp_check.signal_connect('toggled') {
3412 if smp_check.active?
3413 smp_hbox.sensitive = true
3415 smp_hbox.sensitive = false
3419 smp_check.active = true
3420 smp_spin.value = $config['mproc'].to_i
3422 nogestures_check.active = $config['nogestures']
3423 deleteondisk_check.active = $config['deleteondisk']
3425 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3426 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3427 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3428 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3429 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3430 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3431 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3432 tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3433 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3434 tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3435 2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3436 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)
3437 commentsformat_help.signal_connect('clicked') {
3438 show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3439 hence you should look at ImageMagick/identify documentation for the most
3440 accurate and up-to-date documentation. Last time I checked, documentation
3443 Print information about the image in a format of your choosing. You can
3444 include the image filename, type, width, height, Exif data, or other image
3445 attributes by embedding special format characters:
3448 %P page width and height
3452 %e filename extension
3457 %k number of unique colors
3464 %r image class and colorspace
3467 %u unique temporary filename