5 # A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
13 # Copyright (c) 2004-2006 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
27 require 'booh/libadds'
28 require 'booh/GtkAutoTable'
32 bindtextdomain("booh")
34 require 'rexml/document'
37 require 'booh/booh-lib'
39 require 'booh/UndoHandler'
40 require 'booh/Synchronizator'
45 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
47 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
50 #- default values for some globals
54 $ignore_videos = false
55 $button1_pressed_autotable = false
56 $generated_outofline = false
59 puts _("Usage: %s [OPTION]...") % File.basename($0)
61 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
66 parser = GetoptLong.new
67 parser.set_options(*$options.collect { |ary| ary[0..2] })
69 parser.each_option do |name, arg|
75 when '--verbose-level'
76 $verbose_level = arg.to_i
89 $config_file = File.expand_path('~/.booh-gui-rc')
90 if File.readable?($config_file)
91 $xmldoc = REXML::Document.new(File.new($config_file))
92 $xmldoc.root.elements.each { |element|
93 txt = element.get_text
95 if txt.value =~ /~~~/ || element.name == 'last-opens'
96 $config[element.name] = txt.value.split(/~~~/)
98 $config[element.name] = txt.value
100 elsif element.elements.size == 0
101 $config[element.name] = ''
103 $config[element.name] = {}
104 element.each { |chld|
106 $config[element.name][chld.name] = txt ? txt.value : nil
111 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
112 $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
113 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
114 $config['comments-format'] ||= '%t'
115 if !FileTest.directory?(File.expand_path('~/.booh'))
116 system("mkdir ~/.booh")
118 if $config['mproc'].nil?
120 for line in IO.readlines('/proc/cpuinfo') do
121 line =~ /^processor/ and cpus += 1
124 $config['mproc'] = cpus
127 $config['rotate-set-exif'] ||= 'true'
133 if !system("which convert >/dev/null 2>/dev/null")
134 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
135 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
138 if !system("which identify >/dev/null 2>/dev/null")
139 show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
140 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
142 if !system("which exif >/dev/null 2>/dev/null")
143 show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
145 missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
147 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
150 viewer_binary = $config['video-viewer'].split.first
151 if viewer_binary && !File.executable?(viewer_binary)
152 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
153 You should fix this in Edit/Preferences so that you can view videos.
155 Problem was: '%s' is not an executable file.
156 Hint: don't forget to specify the full path to the executable,
157 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
159 image_editor_binary = $config['image-editor'].split.first
160 if image_editor_binary && !File.executable?(image_editor_binary)
161 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
162 You should fix this in Edit/Preferences so that you can edit images externally.
164 Problem was: '%s' is not an executable file.
165 Hint: don't forget to specify the full path to the executable,
166 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
168 browser_binary = $config['browser'].split.first
169 if browser_binary && !File.executable?(browser_binary)
170 show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
171 You should fix this in Edit/Preferences so that you can open URLs.
173 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
178 if $config['last-opens'] && $config['last-opens'].size > 10
179 $config['last-opens'] = $config['last-opens'][-10, 10]
182 ios = File.open($config_file, "w")
183 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
184 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
185 $config.each_pair { |key, value|
186 elem = $xmldoc.root.add_element key
188 $config[key].each_pair { |subkey, subvalue|
189 subelem = elem.add_element subkey
190 subelem.add_text subvalue.to_s
192 elsif value.is_a? Array
193 elem.add_text value.join('~~~')
198 elem.add_text value.to_s
202 $xmldoc.write(ios, 0)
205 $tempfiles.each { |f|
210 def set_mousecursor(what, *widget)
211 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
212 if widget[0] && widget[0].window
213 widget[0].window.cursor = cursor
215 if $main_window && $main_window.window
216 $main_window.window.cursor = cursor
218 $current_cursor = what
220 def set_mousecursor_wait(*widget)
221 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
222 if Thread.current == Thread.main
223 Gtk.main_iteration while Gtk.events_pending?
226 def set_mousecursor_normal(*widget)
227 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
229 def push_mousecursor_wait(*widget)
230 if $current_cursor != Gdk::Cursor::WATCH
231 $save_cursor = $current_cursor
232 gtk_thread_protect { set_mousecursor_wait(*widget) }
235 def pop_mousecursor(*widget)
236 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
240 source = $xmldoc.root.attributes['source']
241 dest = $xmldoc.root.attributes['destination']
242 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
245 def full_src_dir_to_rel(path, source)
246 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
249 def build_full_dest_filename(filename)
250 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
253 def save_undo(name, closure, *params)
254 UndoHandler.save_undo(name, closure, [ *params ])
255 $undo_tb.sensitive = $undo_mb.sensitive = true
256 $redo_tb.sensitive = $redo_mb.sensitive = false
259 def view_element(filename, closures)
260 if entry2type(filename) == 'video'
261 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
267 w = Gtk::Window.new.set_title(filename)
269 msg 3, "filename: #{filename}"
270 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
271 #- typically this file won't exist in case of videos; try with the largest thumbnail around
272 if !File.exists?(dest_img)
273 if entry2type(filename) == 'video'
274 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
275 if not alternatives.empty?
276 dest_img = alternatives[-1]
279 push_mousecursor_wait
280 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
282 if !File.exists?(dest_img)
283 msg 2, _("Could not generate fullscreen thumbnail!")
288 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)))
289 evt.signal_connect('button-press-event') { |this, event|
290 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
291 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
293 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
295 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
296 delete_item.signal_connect('activate') {
298 closures[:delete].call(false)
301 menu.popup(nil, nil, event.button, event.time)
304 evt.signal_connect('button-release-event') { |this, event|
306 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
307 msg 3, "gesture delete: click-drag right button to the bottom"
309 closures[:delete].call(false)
310 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
314 tooltips = Gtk::Tooltips.new
315 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
317 w.signal_connect('key-press-event') { |w,event|
318 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
320 closures[:delete].call(false)
324 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
325 b.signal_connect('clicked') { w.destroy }
328 vb.pack_start(evt, false, false)
329 vb.pack_end(bottom, false, false)
332 w.signal_connect('delete-event') { w.destroy }
333 w.window_position = Gtk::Window::POS_CENTER
337 def scroll_upper(scrolledwindow, ypos_top)
338 newval = scrolledwindow.vadjustment.value -
339 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
340 if newval < scrolledwindow.vadjustment.lower
341 newval = scrolledwindow.vadjustment.lower
343 scrolledwindow.vadjustment.value = newval
346 def scroll_lower(scrolledwindow, ypos_bottom)
347 newval = scrolledwindow.vadjustment.value +
348 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
349 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
350 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
352 scrolledwindow.vadjustment.value = newval
355 def autoscroll_if_needed(scrolledwindow, image, textview)
356 #- autoscroll if cursor or image is not visible, if possible
357 if image && image.window || textview.window
358 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
359 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
360 current_miny_visible = scrolledwindow.vadjustment.value
361 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
362 if ypos_top < current_miny_visible
363 scroll_upper(scrolledwindow, ypos_top)
364 elsif ypos_bottom > current_maxy_visible
365 scroll_lower(scrolledwindow, ypos_bottom)
370 def create_editzone(scrolledwindow, pagenum, image)
371 frame = Gtk::Frame.new
372 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
373 frame.set_shadow_type(Gtk::SHADOW_IN)
374 textview.signal_connect('key-press-event') { |w, event|
375 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
376 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
377 scrolledwindow.signal_emit('key-press-event', event)
379 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
380 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
381 if event.keyval == Gdk::Keyval::GDK_Up
382 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
383 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
385 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
388 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
389 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
391 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
398 candidate_undo_text = nil
399 textview.signal_connect('focus-in-event') { |w, event|
400 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
401 candidate_undo_text = textview.buffer.text
405 textview.signal_connect('key-release-event') { |w, event|
406 if candidate_undo_text && candidate_undo_text != textview.buffer.text
408 save_undo(_("text edit"),
410 save_text = textview.buffer.text
411 textview.buffer.text = text
413 $notebook.set_page(pagenum)
415 textview.buffer.text = save_text
417 $notebook.set_page(pagenum)
419 }, candidate_undo_text)
420 candidate_undo_text = nil
423 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)
424 autoscroll_if_needed(scrolledwindow, image, textview)
429 return [ frame, textview ]
432 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
434 if !$modified_pixbufs[thumbnail_img]
435 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
436 elsif !$modified_pixbufs[thumbnail_img][:orig]
437 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
440 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
443 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
444 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
445 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
446 if pixbuf.height > desired_y
447 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
448 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
449 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
454 if $modified_pixbufs[thumbnail_img][:whitebalance]
455 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
458 #- fix gamma correction
459 if $modified_pixbufs[thumbnail_img][:gammacorrect]
460 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
463 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
466 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
469 #- update rotate attribute
470 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
471 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
473 if $config['rotate-set-exif'] == 'true'
474 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
477 $modified_pixbufs[thumbnail_img] ||= {}
478 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
479 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
481 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
484 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
487 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
489 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
491 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
492 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
494 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
495 $notebook.set_page(0)
496 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
501 def color_swap(xmldir, attributes_prefix)
503 if xmldir.attributes["#{attributes_prefix}color-swap"]
504 xmldir.delete_attribute("#{attributes_prefix}color-swap")
506 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
510 def enhance(xmldir, attributes_prefix)
512 if xmldir.attributes["#{attributes_prefix}enhance"]
513 xmldir.delete_attribute("#{attributes_prefix}enhance")
515 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
519 def change_seektime(xmldir, attributes_prefix, value)
521 xmldir.add_attribute("#{attributes_prefix}seektime", value)
524 def ask_new_seektime(xmldir, attributes_prefix)
526 value = xmldir.attributes["#{attributes_prefix}seektime"]
531 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
533 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
534 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
535 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
539 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
543 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
544 entry.signal_connect('key-press-event') { |w, event|
545 if event.keyval == Gdk::Keyval::GDK_Return
546 dialog.response(Gtk::Dialog::RESPONSE_OK)
548 elsif event.keyval == Gdk::Keyval::GDK_Escape
549 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
552 false #- propagate if needed
556 dialog.window_position = Gtk::Window::POS_MOUSE
559 dialog.run { |response|
562 if response == Gtk::Dialog::RESPONSE_OK
564 msg 3, "changing seektime to #{newval}"
565 return { :old => value, :new => newval }
572 def change_pano_amount(xmldir, attributes_prefix, value)
575 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
577 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
581 def ask_new_pano_amount(xmldir, attributes_prefix)
583 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
588 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
590 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
591 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
592 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
596 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
597 of this panorama image compared to other regular images. For example, if the panorama
598 was taken out of four photos on one row, counting the necessary overlap, the width of
599 this panorama image should probably be roughly three times the width of regular images.
601 With this information, booh will be able to generate panorama thumbnails looking
605 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)")))).
606 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
607 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
608 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
609 spin.signal_connect('value-changed') {
612 dialog.window_position = Gtk::Window::POS_MOUSE
615 spin.value = value.to_f
622 dialog.run { |response|
626 newval = spin.value.to_f
629 if response == Gtk::Dialog::RESPONSE_OK
631 msg 3, "changing panorama amount to #{newval}"
632 return { :old => value, :new => newval }
639 def change_whitebalance(xmlelem, attributes_prefix, value)
641 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
644 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
646 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
647 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
648 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
649 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
650 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
651 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
652 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
653 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
654 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
655 $modified_pixbufs[thumbnail_img] ||= {}
656 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
657 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
659 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
660 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
662 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
665 $modified_pixbufs[thumbnail_img] ||= {}
666 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
668 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
671 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
672 #- init $modified_pixbufs correctly
673 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
675 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
677 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
679 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
680 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
681 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
685 _("You can fix the <b>white balance</b> of the image, if your image is too blue
686 or too yellow because your camera didn't detect the light correctly. Drag the
687 slider below the image to the left for more blue, to the right for more yellow.
691 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
693 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
695 dialog.window_position = Gtk::Window::POS_MOUSE
699 timeout = Gtk.timeout_add(100) {
700 if hs.value != lastval
703 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
709 dialog.run { |response|
710 Gtk.timeout_remove(timeout)
711 if response == Gtk::Dialog::RESPONSE_OK
713 newval = hs.value.to_s
714 msg 3, "changing white balance to #{newval}"
716 return { :old => value, :new => newval }
719 $modified_pixbufs[thumbnail_img] ||= {}
720 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
721 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
729 def change_gammacorrect(xmlelem, attributes_prefix, value)
731 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
734 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
736 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
737 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
738 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
739 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
740 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
741 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
742 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
743 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
744 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
745 $modified_pixbufs[thumbnail_img] ||= {}
746 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
747 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
749 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
750 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
752 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
755 $modified_pixbufs[thumbnail_img] ||= {}
756 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
758 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
761 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
762 #- init $modified_pixbufs correctly
763 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
765 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
767 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
769 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
770 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
771 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
775 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
776 or too bright. Drag the slider below the image.
780 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
782 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
784 dialog.window_position = Gtk::Window::POS_MOUSE
788 timeout = Gtk.timeout_add(100) {
789 if hs.value != lastval
792 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
798 dialog.run { |response|
799 Gtk.timeout_remove(timeout)
800 if response == Gtk::Dialog::RESPONSE_OK
802 newval = hs.value.to_s
803 msg 3, "gamma correction to #{newval}"
805 return { :old => value, :new => newval }
808 $modified_pixbufs[thumbnail_img] ||= {}
809 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
810 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
818 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
819 system("rm -f '#{destfile}'")
820 #- type can be 'element' or 'subdir'
822 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
824 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
828 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
830 push_mousecursor_wait
831 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
834 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
840 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
841 distribute_multiple_call = Proc.new { |action, arg|
842 $selected_elements.each_key { |path|
843 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
845 if possible_actions[:can_multiple] && $selected_elements.length > 0
846 UndoHandler.begin_batch
847 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
848 UndoHandler.end_batch
850 closures[action].call(arg)
852 $selected_elements = {}
855 if optionals.include?('change_image')
856 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
857 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
858 changeimg.signal_connect('activate') { closures[:change].call }
859 menu.append(Gtk::SeparatorMenuItem.new)
861 if !possible_actions[:can_multiple] || $selected_elements.length == 0
864 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
865 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
866 view.signal_connect('activate') { closures[:view].call }
868 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
869 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
870 view.signal_connect('activate') { closures[:view].call }
871 menu.append(Gtk::SeparatorMenuItem.new)
874 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
875 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
876 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
877 exif.signal_connect('activate') { show_popup($main_window,
878 utf8(`exif -m #{fullpath}`),
879 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
880 menu.append(Gtk::SeparatorMenuItem.new)
883 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
884 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
885 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
886 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
887 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
888 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
889 if !possible_actions[:can_multiple] || $selected_elements.length == 0
890 menu.append(Gtk::SeparatorMenuItem.new)
891 if !possible_actions[:forbid_left]
892 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
893 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
894 moveleft.signal_connect('activate') { closures[:move].call('left') }
895 if !possible_actions[:can_left]
896 moveleft.sensitive = false
899 if !possible_actions[:forbid_right]
900 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
901 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
902 moveright.signal_connect('activate') { closures[:move].call('right') }
903 if !possible_actions[:can_right]
904 moveright.sensitive = false
907 if optionals.include?('move_top')
908 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
909 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
910 movetop.signal_connect('activate') { closures[:move].call('top') }
911 if !possible_actions[:can_top]
912 movetop.sensitive = false
915 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
916 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
917 moveup.signal_connect('activate') { closures[:move].call('up') }
918 if !possible_actions[:can_up]
919 moveup.sensitive = false
921 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
922 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
923 movedown.signal_connect('activate') { closures[:move].call('down') }
924 if !possible_actions[:can_down]
925 movedown.sensitive = false
927 if optionals.include?('move_bottom')
928 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
929 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
930 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
931 if !possible_actions[:can_bottom]
932 movebottom.sensitive = false
937 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
938 menu.append(Gtk::SeparatorMenuItem.new)
939 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
940 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
941 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
942 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
943 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
944 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
945 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
946 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
947 seektime.signal_connect('activate') {
948 if possible_actions[:can_multiple] && $selected_elements.length > 0
949 if values = ask_new_seektime(nil, '')
950 distribute_multiple_call.call(:seektime, values)
953 closures[:seektime].call
958 menu.append( Gtk::SeparatorMenuItem.new)
959 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
960 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
961 gammacorrect.signal_connect('activate') {
962 if possible_actions[:can_multiple] && $selected_elements.length > 0
963 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
964 distribute_multiple_call.call(:gammacorrect, values)
967 closures[:gammacorrect].call
970 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
971 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
972 whitebalance.signal_connect('activate') {
973 if possible_actions[:can_multiple] && $selected_elements.length > 0
974 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
975 distribute_multiple_call.call(:whitebalance, values)
978 closures[:whitebalance].call
981 if !possible_actions[:can_multiple] || $selected_elements.length == 0
982 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
983 _("Enhance constrast"))))
985 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
987 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
988 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
989 if type == 'image' && possible_actions[:can_panorama]
990 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
991 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
992 panorama.signal_connect('activate') {
993 if possible_actions[:can_multiple] && $selected_elements.length > 0
994 if values = ask_new_pano_amount(nil, '')
995 distribute_multiple_call.call(:pano, values)
998 distribute_multiple_call.call(:pano)
1002 menu.append( Gtk::SeparatorMenuItem.new)
1003 if optionals.include?('delete')
1004 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1005 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1006 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1007 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1008 paste_item.signal_connect('activate') { closures[:paste].call }
1009 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1010 clear_item.signal_connect('activate') { $cuts = [] }
1012 paste_item.sensitive = clear_item.sensitive = false
1015 menu.append( Gtk::SeparatorMenuItem.new)
1017 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1018 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1019 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1020 editexternally.signal_connect('activate') {
1021 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1026 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1027 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1028 if optionals.include?('delete')
1029 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1030 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1033 menu.popup(nil, nil, event.button, event.time)
1036 def delete_current_subalbum
1038 sel = $albums_tv.selection.selected_rows
1039 $xmldir.elements.each { |e|
1040 if e.name == 'image' || e.name == 'video'
1041 e.add_attribute('deleted', 'true')
1044 #- branch if we have a non deleted subalbum
1045 if $xmldir.child_byname_notattr('dir', 'deleted')
1046 $xmldir.delete_attribute('thumbnails-caption')
1047 $xmldir.delete_attribute('thumbnails-captionfile')
1049 $xmldir.add_attribute('deleted', 'true')
1051 while moveup.parent.name == 'dir'
1052 moveup = moveup.parent
1053 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1054 moveup.add_attribute('deleted', 'true')
1061 save_changes('forced')
1062 populate_subalbums_treeview(false)
1063 $albums_tv.selection.select_path(sel[0])
1069 $current_path = nil #- prevent save_changes from being rerun again
1070 sel = $albums_tv.selection.selected_rows
1071 restore_one = proc { |xmldir|
1072 xmldir.elements.each { |e|
1073 if e.name == 'dir' && e.attributes['deleted']
1076 e.delete_attribute('deleted')
1079 restore_one.call($xmldir)
1080 populate_subalbums_treeview(false)
1081 $albums_tv.selection.select_path(sel[0])
1084 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1087 frame1 = Gtk::Frame.new
1088 fullpath = from_utf8("#{$current_path}/#{filename}")
1090 my_gen_real_thumbnail = proc {
1091 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1095 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1096 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1097 pack_start(img = Gtk::Image.new).
1098 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1099 px, mask = pxb.render_pixmap_and_mask(0)
1100 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1101 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1103 frame1.add(img = Gtk::Image.new)
1106 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1107 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1108 my_gen_real_thumbnail.call
1110 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1113 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1115 tooltips = Gtk::Tooltips.new
1116 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1117 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1119 frame2, textview = create_editzone($autotable_sw, 1, img)
1120 textview.buffer.text = caption
1121 textview.set_justification(Gtk::Justification::CENTER)
1123 vbox = Gtk::VBox.new(false, 5)
1124 vbox.pack_start(evtbox, false, false)
1125 vbox.pack_start(frame2, false, false)
1126 autotable.append(vbox, filename)
1128 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1129 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1131 #- to be able to find widgets by name
1132 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1134 cleanup_all_thumbnails = proc {
1135 #- remove out of sync images
1136 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1137 for sizeobj in $images_size
1138 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
1144 cleanup_all_thumbnails.call
1145 my_gen_real_thumbnail.call
1148 rotate_and_cleanup = proc { |angle|
1149 cleanup_all_thumbnails.call
1150 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1153 move = proc { |direction|
1154 do_method = "move_#{direction}"
1155 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1157 done = autotable.method(do_method).call(vbox)
1158 textview.grab_focus #- because if moving, focus is stolen
1162 save_undo(_("move %s") % direction,
1164 autotable.method(undo_method).call(vbox)
1165 textview.grab_focus #- because if moving, focus is stolen
1166 autoscroll_if_needed($autotable_sw, img, textview)
1167 $notebook.set_page(1)
1169 autotable.method(do_method).call(vbox)
1170 textview.grab_focus #- because if moving, focus is stolen
1171 autoscroll_if_needed($autotable_sw, img, textview)
1172 $notebook.set_page(1)
1178 color_swap_and_cleanup = proc {
1179 perform_color_swap_and_cleanup = proc {
1180 cleanup_all_thumbnails.call
1181 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1182 my_gen_real_thumbnail.call
1185 perform_color_swap_and_cleanup.call
1187 save_undo(_("color swap"),
1189 perform_color_swap_and_cleanup.call
1191 autoscroll_if_needed($autotable_sw, img, textview)
1192 $notebook.set_page(1)
1194 perform_color_swap_and_cleanup.call
1196 autoscroll_if_needed($autotable_sw, img, textview)
1197 $notebook.set_page(1)
1202 change_seektime_and_cleanup_real = proc { |values|
1203 perform_change_seektime_and_cleanup = proc { |val|
1204 cleanup_all_thumbnails.call
1205 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1206 my_gen_real_thumbnail.call
1208 perform_change_seektime_and_cleanup.call(values[:new])
1210 save_undo(_("specify seektime"),
1212 perform_change_seektime_and_cleanup.call(values[:old])
1214 autoscroll_if_needed($autotable_sw, img, textview)
1215 $notebook.set_page(1)
1217 perform_change_seektime_and_cleanup.call(values[:new])
1219 autoscroll_if_needed($autotable_sw, img, textview)
1220 $notebook.set_page(1)
1225 change_seektime_and_cleanup = proc {
1226 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1227 change_seektime_and_cleanup_real.call(values)
1231 change_pano_amount_and_cleanup_real = proc { |values|
1232 perform_change_pano_amount_and_cleanup = proc { |val|
1233 cleanup_all_thumbnails.call
1234 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1236 perform_change_pano_amount_and_cleanup.call(values[:new])
1238 save_undo(_("change panorama amount"),
1240 perform_change_pano_amount_and_cleanup.call(values[:old])
1242 autoscroll_if_needed($autotable_sw, img, textview)
1243 $notebook.set_page(1)
1245 perform_change_pano_amount_and_cleanup.call(values[:new])
1247 autoscroll_if_needed($autotable_sw, img, textview)
1248 $notebook.set_page(1)
1253 change_pano_amount_and_cleanup = proc {
1254 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1255 change_pano_amount_and_cleanup_real.call(values)
1259 whitebalance_and_cleanup_real = proc { |values|
1260 perform_change_whitebalance_and_cleanup = proc { |val|
1261 cleanup_all_thumbnails.call
1262 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1263 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1264 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1266 perform_change_whitebalance_and_cleanup.call(values[:new])
1268 save_undo(_("fix white balance"),
1270 perform_change_whitebalance_and_cleanup.call(values[:old])
1272 autoscroll_if_needed($autotable_sw, img, textview)
1273 $notebook.set_page(1)
1275 perform_change_whitebalance_and_cleanup.call(values[:new])
1277 autoscroll_if_needed($autotable_sw, img, textview)
1278 $notebook.set_page(1)
1283 whitebalance_and_cleanup = proc {
1284 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1285 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1286 whitebalance_and_cleanup_real.call(values)
1290 gammacorrect_and_cleanup_real = proc { |values|
1291 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1292 cleanup_all_thumbnails.call
1293 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1294 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1295 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1297 perform_change_gammacorrect_and_cleanup.call(values[:new])
1299 save_undo(_("gamma correction"),
1301 perform_change_gammacorrect_and_cleanup.call(values[:old])
1303 autoscroll_if_needed($autotable_sw, img, textview)
1304 $notebook.set_page(1)
1306 perform_change_gammacorrect_and_cleanup.call(values[:new])
1308 autoscroll_if_needed($autotable_sw, img, textview)
1309 $notebook.set_page(1)
1314 gammacorrect_and_cleanup = Proc.new {
1315 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1316 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1317 gammacorrect_and_cleanup_real.call(values)
1321 enhance_and_cleanup = proc {
1322 perform_enhance_and_cleanup = proc {
1323 cleanup_all_thumbnails.call
1324 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1325 my_gen_real_thumbnail.call
1328 cleanup_all_thumbnails.call
1329 perform_enhance_and_cleanup.call
1331 save_undo(_("enhance"),
1333 perform_enhance_and_cleanup.call
1335 autoscroll_if_needed($autotable_sw, img, textview)
1336 $notebook.set_page(1)
1338 perform_enhance_and_cleanup.call
1340 autoscroll_if_needed($autotable_sw, img, textview)
1341 $notebook.set_page(1)
1346 delete = proc { |isacut|
1347 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 })
1350 perform_delete = proc {
1351 after = autotable.get_next_widget(vbox)
1353 after = autotable.get_previous_widget(vbox)
1355 if $config['deleteondisk'] && !isacut
1356 msg 3, "scheduling for delete: #{fullpath}"
1357 $todelete << fullpath
1359 autotable.remove_widget(vbox)
1361 $vbox2widgets[after][:textview].grab_focus
1362 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1366 previous_pos = autotable.get_current_number(vbox)
1370 delete_current_subalbum
1372 save_undo(_("delete"),
1374 autotable.reinsert(pos, vbox, filename)
1375 $notebook.set_page(1)
1376 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1378 msg 3, "removing deletion schedule of: #{fullpath}"
1379 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1382 $notebook.set_page(1)
1391 $cuts << { :vbox => vbox, :filename => filename }
1392 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1397 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1400 autotable.queue_draws << proc {
1401 $vbox2widgets[last[:vbox]][:textview].grab_focus
1402 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1404 save_undo(_("paste"),
1406 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1407 $notebook.set_page(1)
1410 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1412 $notebook.set_page(1)
1415 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1420 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1421 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1422 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1423 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1425 textview.signal_connect('key-press-event') { |w, event|
1428 x, y = autotable.get_current_pos(vbox)
1429 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1430 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1431 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1432 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1434 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1435 $vbox2widgets[widget_up][:textview].grab_focus
1442 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1444 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1445 $vbox2widgets[widget_down][:textview].grab_focus
1452 if event.keyval == Gdk::Keyval::GDK_Left
1455 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1462 rotate_and_cleanup.call(-90)
1465 if event.keyval == Gdk::Keyval::GDK_Right
1466 next_ = autotable.get_next_widget(vbox)
1467 if next_ && autotable.get_current_pos(next_)[0] > x
1469 $vbox2widgets[next_][:textview].grab_focus
1476 rotate_and_cleanup.call(90)
1479 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1482 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1483 view_element(filename, { :delete => delete })
1486 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1489 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1493 !propagate #- propagate if needed
1496 $ignore_next_release = false
1497 evtbox.signal_connect('button-press-event') { |w, event|
1498 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1499 if event.state & Gdk::Window::BUTTON3_MASK != 0
1500 #- gesture redo: hold right mouse button then click left mouse button
1501 $config['nogestures'] or perform_redo
1502 $ignore_next_release = true
1504 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1506 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1508 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1509 elsif $enhance.active?
1510 enhance_and_cleanup.call
1511 elsif $delete.active?
1515 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1518 $button1_pressed_autotable = true
1519 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1520 if event.state & Gdk::Window::BUTTON1_MASK != 0
1521 #- gesture undo: hold left mouse button then click right mouse button
1522 $config['nogestures'] or perform_undo
1523 $ignore_next_release = true
1525 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1526 view_element(filename, { :delete => delete })
1531 evtbox.signal_connect('button-release-event') { |w, event|
1532 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1533 if !$ignore_next_release
1534 x, y = autotable.get_current_pos(vbox)
1535 next_ = autotable.get_next_widget(vbox)
1536 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1537 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1538 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1539 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1540 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1541 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1542 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1544 $ignore_next_release = false
1545 $gesture_press = nil
1550 #- handle reordering with drag and drop
1551 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1552 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1553 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1554 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1557 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1559 #- mouse gesture first (dnd disables button-release-event)
1560 if $gesture_press && $gesture_press[:filename] == filename
1561 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1562 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1563 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1564 rotate_and_cleanup.call(angle)
1565 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1567 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1568 msg 3, "gesture delete: click-drag right button to the bottom"
1570 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1575 ctxt.targets.each { |target|
1576 if target.name == 'reorder-elements'
1577 move_dnd = proc { |from,to|
1580 autotable.move(from, to)
1581 save_undo(_("reorder"),
1584 autotable.move(to - 1, from)
1586 autotable.move(to, from + 1)
1588 $notebook.set_page(1)
1590 autotable.move(from, to)
1591 $notebook.set_page(1)
1596 if $multiple_dnd.size == 0
1597 move_dnd.call(selection_data.data.to_i,
1598 autotable.get_current_number(vbox))
1600 UndoHandler.begin_batch
1601 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1603 #- need to update current position between each call
1604 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1605 autotable.get_current_number(vbox))
1607 UndoHandler.end_batch
1618 def create_auto_table
1620 $autotable = Gtk::AutoTable.new(5)
1622 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1623 thumbnails_vb = Gtk::VBox.new(false, 5)
1625 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1626 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1627 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1628 thumbnails_vb.add($autotable)
1630 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1631 $autotable_sw.add_with_viewport(thumbnails_vb)
1633 #- follows stuff for handling multiple elements selection
1634 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1636 update_selected = proc {
1637 $autotable.current_order.each { |path|
1638 w = $name2widgets[path][:evtbox].window
1639 xm = w.position[0] + w.size[0]/2
1640 ym = w.position[1] + w.size[1]/2
1641 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1642 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1643 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1644 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1647 if $selected_elements[path] && ! $selected_elements[path][:keep]
1648 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))
1649 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1650 $selected_elements.delete(path)
1655 $autotable.signal_connect('realize') { |w,e|
1656 gc = Gdk::GC.new($autotable.window)
1657 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1658 gc.function = Gdk::GC::INVERT
1659 #- autoscroll handling for DND and multiple selections
1660 Gtk.timeout_add(100) {
1661 if ! $autotable.window.nil?
1662 w, x, y, mask = $autotable.window.pointer
1663 if mask & Gdk::Window::BUTTON1_MASK != 0
1664 if y < $autotable_sw.vadjustment.value
1666 $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]])
1668 if $button1_pressed_autotable || press_x
1669 scroll_upper($autotable_sw, y)
1672 w, pos_x, pos_y = $autotable.window.pointer
1673 $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]])
1674 update_selected.call
1677 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1679 $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]])
1681 if $button1_pressed_autotable || press_x
1682 scroll_lower($autotable_sw, y)
1685 w, pos_x, pos_y = $autotable.window.pointer
1686 $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]])
1687 update_selected.call
1692 ! $autotable.window.nil?
1696 $autotable.signal_connect('button-press-event') { |w,e|
1698 if !$button1_pressed_autotable
1701 if e.state & Gdk::Window::SHIFT_MASK == 0
1702 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1703 $selected_elements = {}
1704 $statusbar.push(0, utf8(_("Nothing selected.")))
1706 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1708 set_mousecursor(Gdk::Cursor::TCROSS)
1712 $autotable.signal_connect('button-release-event') { |w,e|
1714 if $button1_pressed_autotable
1715 #- unselect all only now
1716 $multiple_dnd = $selected_elements.keys
1717 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1718 $selected_elements = {}
1719 $button1_pressed_autotable = false
1722 $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]])
1723 if $selected_elements.length > 0
1724 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1727 press_x = press_y = pos_x = pos_y = nil
1728 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1732 $autotable.signal_connect('motion-notify-event') { |w,e|
1735 $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]])
1739 $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]])
1740 update_selected.call
1746 def create_subalbums_page
1748 subalbums_hb = Gtk::HBox.new
1749 $subalbums_vb = Gtk::VBox.new(false, 5)
1750 subalbums_hb.pack_start($subalbums_vb, false, false)
1751 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1752 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1753 $subalbums_sw.add_with_viewport(subalbums_hb)
1756 def save_current_file
1762 ios = File.open($filename, "w")
1763 $xmldoc.write(ios, 0)
1765 rescue Iconv::IllegalSequence
1766 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1767 if ! ios.nil? && ! ios.closed?
1770 $xmldoc.xml_decl.encoding = 'UTF-8'
1771 ios = File.open($filename, "w")
1772 $xmldoc.write(ios, 0)
1783 def save_current_file_user
1784 save_tempfilename = $filename
1785 $filename = $orig_filename
1786 if ! save_current_file
1787 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1788 $filename = save_tempfilename
1792 $generated_outofline = false
1793 $filename = save_tempfilename
1795 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1796 $todelete.each { |f|
1797 system("rm -f #{f}")
1801 def mark_document_as_dirty
1802 $xmldoc.elements.each('//dir') { |elem|
1803 elem.delete_attribute('already-generated')
1807 #- ret: true => ok false => cancel
1808 def ask_save_modifications(msg1, msg2, *options)
1810 options = options.size > 0 ? options[0] : {}
1812 if options[:disallow_cancel]
1813 dialog = Gtk::Dialog.new(msg1,
1815 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1816 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1817 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1819 dialog = Gtk::Dialog.new(msg1,
1821 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1822 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1823 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1824 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1826 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1827 dialog.vbox.add(Gtk::Label.new(msg2))
1828 dialog.window_position = Gtk::Window::POS_CENTER
1831 dialog.run { |response|
1833 if response == Gtk::Dialog::RESPONSE_YES
1834 if ! save_current_file_user
1835 return ask_save_modifications(msg1, msg2, options)
1838 #- if we have generated an album but won't save modifications, we must remove
1839 #- already-generated markers in original file
1840 if $generated_outofline
1842 $xmldoc = REXML::Document.new File.new($orig_filename)
1843 mark_document_as_dirty
1844 ios = File.open($orig_filename, "w")
1845 $xmldoc.write(ios, 0)
1848 puts "exception: #{$!}"
1852 if response == Gtk::Dialog::RESPONSE_CANCEL
1855 $todelete = [] #- unconditionally clear the list of images/videos to delete
1861 def try_quit(*options)
1862 if ask_save_modifications(utf8(_("Save before quitting?")),
1863 utf8(_("Do you want to save your changes before quitting?")),
1869 def show_popup(parent, msg, *options)
1870 dialog = Gtk::Dialog.new
1871 if options[0] && options[0][:title]
1872 dialog.title = options[0][:title]
1874 dialog.title = utf8(_("Booh message"))
1876 lbl = Gtk::Label.new
1877 if options[0] && options[0][:nomarkup]
1882 if options[0] && options[0][:centered]
1883 lbl.set_justify(Gtk::Justification::CENTER)
1885 if options[0] && options[0][:selectable]
1886 lbl.selectable = true
1888 if options[0] && options[0][:topwidget]
1889 dialog.vbox.add(options[0][:topwidget])
1891 if options[0] && options[0][:scrolled]
1892 sw = Gtk::ScrolledWindow.new(nil, nil)
1893 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1894 sw.add_with_viewport(lbl)
1896 dialog.set_default_size(500, 600)
1898 dialog.vbox.add(lbl)
1899 dialog.set_default_size(200, 120)
1901 if options[0] && options[0][:okcancel]
1902 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1904 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1906 if options[0] && options[0][:pos_centered]
1907 dialog.window_position = Gtk::Window::POS_CENTER
1909 dialog.window_position = Gtk::Window::POS_MOUSE
1912 if options[0] && options[0][:linkurl]
1913 linkbut = Gtk::Button.new('')
1914 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1915 linkbut.signal_connect('clicked') {
1916 open_url(options[0][:linkurl] + '/index.html')
1917 dialog.response(Gtk::Dialog::RESPONSE_OK)
1918 set_mousecursor_normal
1920 linkbut.relief = Gtk::RELIEF_NONE
1921 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1922 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1923 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1928 if !options[0] || !options[0][:not_transient]
1929 dialog.transient_for = parent
1930 dialog.run { |response|
1932 if options[0] && options[0][:okcancel]
1933 return response == Gtk::Dialog::RESPONSE_OK
1937 dialog.signal_connect('response') { dialog.destroy }
1941 def backend_wait_message(parent, msg, infopipe_path, mode)
1943 w.set_transient_for(parent)
1946 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1947 vb.pack_start(Gtk::Label.new(msg), false, false)
1949 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1950 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1951 if mode != 'one dir scan'
1952 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1954 if mode == 'web-album'
1955 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1956 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1958 vb.pack_start(Gtk::HSeparator.new, false, false)
1960 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1961 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1962 vb.pack_end(bottom, false, false)
1964 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1965 refresh_thread = Thread.new {
1966 directories_counter = 0
1967 while line = infopipe.gets
1968 if line =~ /^directories: (\d+), sizes: (\d+)/
1969 directories = $1.to_f + 1
1971 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1972 elements = $3.to_f + 1
1973 if mode == 'web-album'
1977 gtk_thread_protect { pb1_1.fraction = 0 }
1978 if mode != 'one dir scan'
1979 newtext = utf8(full_src_dir_to_rel($1, $2))
1980 newtext = '/' if newtext == ''
1981 gtk_thread_protect { pb1_2.text = newtext }
1982 directories_counter += 1
1983 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1985 elsif line =~ /^processing element$/
1986 element_counter += 1
1987 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1988 elsif line =~ /^processing size$/
1989 element_counter += 1
1990 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1991 elsif line =~ /^finished processing sizes$/
1992 gtk_thread_protect { pb1_1.fraction = 1 }
1993 elsif line =~ /^creating index.html$/
1994 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1995 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1996 directories_counter = 0
1997 elsif line =~ /^index.html: (.+)\|(.+)/
1998 newtext = utf8(full_src_dir_to_rel($1, $2))
1999 newtext = '/' if newtext == ''
2000 gtk_thread_protect { pb2.text = newtext }
2001 directories_counter += 1
2002 gtk_thread_protect { pb2.fraction = directories_counter / directories }
2003 elsif line =~ /^die: (.*)$/
2010 w.signal_connect('delete-event') { w.destroy }
2011 w.signal_connect('destroy') {
2012 Thread.kill(refresh_thread)
2013 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2016 system("rm -f #{infopipe_path}")
2019 w.window_position = Gtk::Window::POS_CENTER
2025 def call_backend(cmd, waitmsg, mode, params)
2026 pipe = Tempfile.new("boohpipe")
2028 system("mkfifo #{pipe.path}")
2029 cmd += " --info-pipe #{pipe.path}"
2030 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2035 id, exitstatus = Process.waitpid2(pid)
2036 gtk_thread_protect { w8.destroy }
2038 if params[:successmsg]
2039 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2041 if params[:closure_after]
2042 gtk_thread_protect(¶ms[:closure_after])
2044 elsif exitstatus == 15
2045 #- say nothing, user aborted
2047 gtk_thread_protect { show_popup($main_window,
2048 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2054 button.signal_connect('clicked') {
2055 Process.kill('SIGTERM', pid)
2059 def save_changes(*forced)
2060 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2064 $xmldir.delete_attribute('already-generated')
2066 propagate_children = proc { |xmldir|
2067 if xmldir.attributes['subdirs-caption']
2068 xmldir.delete_attribute('already-generated')
2070 xmldir.elements.each('dir') { |element|
2071 propagate_children.call(element)
2075 if $xmldir.child_byname_notattr('dir', 'deleted')
2076 new_title = $subalbums_title.buffer.text
2077 if new_title != $xmldir.attributes['subdirs-caption']
2078 parent = $xmldir.parent
2079 if parent.name == 'dir'
2080 parent.delete_attribute('already-generated')
2082 propagate_children.call($xmldir)
2084 $xmldir.add_attribute('subdirs-caption', new_title)
2085 $xmldir.elements.each('dir') { |element|
2086 if !element.attributes['deleted']
2087 path = element.attributes['path']
2088 newtext = $subalbums_edits[path][:editzone].buffer.text
2089 if element.attributes['subdirs-caption']
2090 if element.attributes['subdirs-caption'] != newtext
2091 propagate_children.call(element)
2093 element.add_attribute('subdirs-caption', newtext)
2094 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2096 if element.attributes['thumbnails-caption'] != newtext
2097 element.delete_attribute('already-generated')
2099 element.add_attribute('thumbnails-caption', newtext)
2100 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2106 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2107 if $xmldir.attributes['thumbnails-caption']
2108 path = $xmldir.attributes['path']
2109 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2111 elsif $xmldir.attributes['thumbnails-caption']
2112 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2115 if $xmldir.attributes['thumbnails-caption']
2116 if edit = $subalbums_edits[$xmldir.attributes['path']]
2117 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2121 #- remove and reinsert elements to reflect new ordering
2124 $xmldir.elements.each { |element|
2125 if element.name == 'image' || element.name == 'video'
2126 saves[element.attributes['filename']] = element.remove
2130 $autotable.current_order.each { |path|
2131 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2132 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2135 saves.each_key { |path|
2136 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2137 chld.add_attribute('deleted', 'true')
2141 def sort_by_exif_date
2145 $xmldir.elements.each { |element|
2146 if element.name == 'image' || element.name == 'video'
2147 current_order << element.attributes['filename']
2151 #- look for EXIF dates
2154 if current_order.size > 20
2156 w.set_transient_for($main_window)
2158 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2159 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2160 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2161 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2162 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2163 vb.pack_end(bottom, false, false)
2165 w.signal_connect('delete-event') { w.destroy }
2166 w.window_position = Gtk::Window::POS_CENTER
2170 b.signal_connect('clicked') { aborted = true }
2172 current_order.each { |f|
2174 if entry2type(f) == 'image'
2176 pb.fraction = i.to_f / current_order.size
2177 Gtk.main_iteration while Gtk.events_pending?
2178 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2180 dates[f] = date_time
2193 current_order.each { |f|
2194 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2196 dates[f] = date_time
2202 $xmldir.elements.each { |element|
2203 if element.name == 'image' || element.name == 'video'
2204 saves[element.attributes['filename']] = element.remove
2208 neworder = smartsort(current_order, dates)
2211 $xmldir.add_element(saves[f].name, saves[f].attributes)
2214 #- let the auto-table reflect new ordering
2218 def remove_all_captions
2221 $autotable.current_order.each { |path|
2222 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2223 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2225 save_undo(_("remove all captions"),
2227 texts.each_key { |key|
2228 $name2widgets[key][:textview].buffer.text = texts[key]
2230 $notebook.set_page(1)
2232 texts.each_key { |key|
2233 $name2widgets[key][:textview].buffer.text = ''
2235 $notebook.set_page(1)
2241 $selected_elements.each_key { |path|
2242 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2248 $selected_elements = {}
2252 $undo_tb.sensitive = $undo_mb.sensitive = false
2253 $redo_tb.sensitive = $redo_mb.sensitive = false
2259 $subalbums_vb.children.each { |chld|
2260 $subalbums_vb.remove(chld)
2262 $subalbums = Gtk::Table.new(0, 0, true)
2263 current_y_sub_albums = 0
2265 $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"])
2266 $subalbums_edits = {}
2267 subalbums_counter = 0
2268 subalbums_edits_bypos = {}
2270 add_subalbum = proc { |xmldir, counter|
2271 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2272 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2273 if xmldir == $xmldir
2274 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2275 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2276 caption = xmldir.attributes['thumbnails-caption']
2277 infotype = 'thumbnails'
2279 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2280 captionfile, caption = find_subalbum_caption_info(xmldir)
2281 infotype = find_subalbum_info_type(xmldir)
2283 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2284 hbox = Gtk::HBox.new
2285 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2287 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2290 my_gen_real_thumbnail = proc {
2291 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2294 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2295 f.add(img = Gtk::Image.new)
2296 my_gen_real_thumbnail.call
2298 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2300 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2301 $subalbums.attach(hbox,
2302 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2304 frame, textview = create_editzone($subalbums_sw, 0, img)
2305 textview.buffer.text = caption
2306 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2307 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2309 change_image = proc {
2310 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2312 Gtk::FileChooser::ACTION_OPEN,
2314 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2315 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2316 fc.transient_for = $main_window
2317 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))
2318 f.add(preview_img = Gtk::Image.new)
2320 fc.signal_connect('update-preview') { |w|
2322 if fc.preview_filename
2323 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2324 fc.preview_widget_active = true
2326 rescue Gdk::PixbufError
2327 fc.preview_widget_active = false
2330 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2332 old_file = captionfile
2333 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2334 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2335 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2336 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2338 new_file = fc.filename
2339 msg 3, "new captionfile is: #{fc.filename}"
2340 perform_changefile = proc {
2341 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2342 $modified_pixbufs.delete(thumbnail_file)
2343 xmldir.delete_attribute("#{infotype}-rotate")
2344 xmldir.delete_attribute("#{infotype}-color-swap")
2345 xmldir.delete_attribute("#{infotype}-enhance")
2346 xmldir.delete_attribute("#{infotype}-seektime")
2347 my_gen_real_thumbnail.call
2349 perform_changefile.call
2351 save_undo(_("change caption file for sub-album"),
2353 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2354 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2355 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2356 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2357 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2358 my_gen_real_thumbnail.call
2359 $notebook.set_page(0)
2361 perform_changefile.call
2362 $notebook.set_page(0)
2370 system("rm -f '#{thumbnail_file}'")
2371 my_gen_real_thumbnail.call
2374 rotate_and_cleanup = proc { |angle|
2375 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2376 system("rm -f '#{thumbnail_file}'")
2379 move = proc { |direction|
2382 save_changes('forced')
2383 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2384 if direction == 'up'
2385 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2386 subalbums_edits_bypos[oldpos - 1][:position] += 1
2388 if direction == 'down'
2389 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2390 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2392 if direction == 'top'
2393 for i in 1 .. oldpos - 1
2394 subalbums_edits_bypos[i][:position] += 1
2396 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2398 if direction == 'bottom'
2399 for i in oldpos + 1 .. subalbums_counter
2400 subalbums_edits_bypos[i][:position] -= 1
2402 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2406 $xmldir.elements.each('dir') { |element|
2407 if (!element.attributes['deleted'])
2408 elems << [ element.attributes['path'], element.remove ]
2411 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2412 each { |e| $xmldir.add_element(e[1]) }
2413 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2414 $xmldir.elements.each('descendant::dir') { |elem|
2415 elem.delete_attribute('already-generated')
2418 sel = $albums_tv.selection.selected_rows
2420 populate_subalbums_treeview(false)
2421 $albums_tv.selection.select_path(sel[0])
2424 color_swap_and_cleanup = proc {
2425 perform_color_swap_and_cleanup = proc {
2426 color_swap(xmldir, "#{infotype}-")
2427 my_gen_real_thumbnail.call
2429 perform_color_swap_and_cleanup.call
2431 save_undo(_("color swap"),
2433 perform_color_swap_and_cleanup.call
2434 $notebook.set_page(0)
2436 perform_color_swap_and_cleanup.call
2437 $notebook.set_page(0)
2442 change_seektime_and_cleanup = proc {
2443 if values = ask_new_seektime(xmldir, "#{infotype}-")
2444 perform_change_seektime_and_cleanup = proc { |val|
2445 change_seektime(xmldir, "#{infotype}-", val)
2446 my_gen_real_thumbnail.call
2448 perform_change_seektime_and_cleanup.call(values[:new])
2450 save_undo(_("specify seektime"),
2452 perform_change_seektime_and_cleanup.call(values[:old])
2453 $notebook.set_page(0)
2455 perform_change_seektime_and_cleanup.call(values[:new])
2456 $notebook.set_page(0)
2462 whitebalance_and_cleanup = proc {
2463 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2464 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2465 perform_change_whitebalance_and_cleanup = proc { |val|
2466 change_whitebalance(xmldir, "#{infotype}-", val)
2467 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2468 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2469 system("rm -f '#{thumbnail_file}'")
2471 perform_change_whitebalance_and_cleanup.call(values[:new])
2473 save_undo(_("fix white balance"),
2475 perform_change_whitebalance_and_cleanup.call(values[:old])
2476 $notebook.set_page(0)
2478 perform_change_whitebalance_and_cleanup.call(values[:new])
2479 $notebook.set_page(0)
2485 gammacorrect_and_cleanup = proc {
2486 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2487 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2488 perform_change_gammacorrect_and_cleanup = proc { |val|
2489 change_gammacorrect(xmldir, "#{infotype}-", val)
2490 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2491 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2492 system("rm -f '#{thumbnail_file}'")
2494 perform_change_gammacorrect_and_cleanup.call(values[:new])
2496 save_undo(_("gamma correction"),
2498 perform_change_gammacorrect_and_cleanup.call(values[:old])
2499 $notebook.set_page(0)
2501 perform_change_gammacorrect_and_cleanup.call(values[:new])
2502 $notebook.set_page(0)
2508 enhance_and_cleanup = proc {
2509 perform_enhance_and_cleanup = proc {
2510 enhance(xmldir, "#{infotype}-")
2511 my_gen_real_thumbnail.call
2514 perform_enhance_and_cleanup.call
2516 save_undo(_("enhance"),
2518 perform_enhance_and_cleanup.call
2519 $notebook.set_page(0)
2521 perform_enhance_and_cleanup.call
2522 $notebook.set_page(0)
2527 evtbox.signal_connect('button-press-event') { |w, event|
2528 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2530 rotate_and_cleanup.call(90)
2532 rotate_and_cleanup.call(-90)
2533 elsif $enhance.active?
2534 enhance_and_cleanup.call
2537 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2538 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2539 { :forbid_left => true, :forbid_right => true,
2540 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2541 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2542 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2543 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2544 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2546 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2551 evtbox.signal_connect('button-press-event') { |w, event|
2552 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2556 evtbox.signal_connect('button-release-event') { |w, event|
2557 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2558 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2559 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2560 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2561 msg 3, "gesture rotate: #{angle}"
2562 rotate_and_cleanup.call(angle)
2565 $gesture_press = nil
2568 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2569 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2570 current_y_sub_albums += 1
2573 if $xmldir.child_byname_notattr('dir', 'deleted')
2575 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2576 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2577 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2578 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2579 #- this album image/caption
2580 if $xmldir.attributes['thumbnails-caption']
2581 add_subalbum.call($xmldir, 0)
2584 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2585 $xmldir.elements.each { |element|
2586 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2587 #- element (image or video) of this album
2588 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2589 msg 3, "dest_img: #{dest_img}"
2590 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2591 total[element.name] += 1
2593 if element.name == 'dir' && !element.attributes['deleted']
2594 #- sub-album image/caption
2595 add_subalbum.call(element, subalbums_counter += 1)
2596 total[element.name] += 1
2599 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2600 total['image'], total['video'], total['dir'] ]))
2601 $subalbums_vb.add($subalbums)
2602 $subalbums_vb.show_all
2604 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2605 $notebook.get_tab_label($autotable_sw).sensitive = false
2606 $notebook.set_page(0)
2607 $thumbnails_title.buffer.text = ''
2609 $notebook.get_tab_label($autotable_sw).sensitive = true
2610 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2613 if !$xmldir.child_byname_notattr('dir', 'deleted')
2614 $notebook.get_tab_label($subalbums_sw).sensitive = false
2615 $notebook.set_page(1)
2617 $notebook.get_tab_label($subalbums_sw).sensitive = true
2621 def pixbuf_or_nil(filename)
2623 return Gdk::Pixbuf.new(filename)
2629 def theme_choose(current)
2630 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2632 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2633 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2634 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2636 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2637 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2638 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2639 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2640 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2641 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2642 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2643 treeview.signal_connect('button-press-event') { |w, event|
2644 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2645 dialog.response(Gtk::Dialog::RESPONSE_OK)
2649 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2651 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2654 iter[0] = File.basename(dir)
2655 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2656 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2657 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2658 if File.basename(dir) == current
2659 treeview.selection.select_iter(iter)
2663 dialog.set_default_size(700, 400)
2664 dialog.vbox.show_all
2665 dialog.run { |response|
2666 iter = treeview.selection.selected
2668 if response == Gtk::Dialog::RESPONSE_OK && iter
2669 return model.get_value(iter, 0)
2675 def show_password_protections
2676 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2677 child_iter = $albums_iters[xmldir.attributes['path']]
2678 if xmldir.attributes['password-protect']
2679 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2680 already_protected = true
2681 elsif already_protected
2682 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2684 pix = pix.saturate_and_pixelate(1, true)
2690 xmldir.elements.each('dir') { |elem|
2691 if !elem.attributes['deleted']
2692 examine_dir_elem.call(child_iter, elem, already_protected)
2696 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2699 def populate_subalbums_treeview(select_first)
2703 $subalbums_vb.children.each { |chld|
2704 $subalbums_vb.remove(chld)
2707 source = $xmldoc.root.attributes['source']
2708 msg 3, "source: #{source}"
2710 xmldir = $xmldoc.elements['//dir']
2711 if !xmldir || xmldir.attributes['path'] != source
2712 msg 1, _("Corrupted booh file...")
2716 append_dir_elem = proc { |parent_iter, xmldir|
2717 child_iter = $albums_ts.append(parent_iter)
2718 child_iter[0] = File.basename(xmldir.attributes['path'])
2719 child_iter[1] = xmldir.attributes['path']
2720 $albums_iters[xmldir.attributes['path']] = child_iter
2721 msg 3, "puttin location: #{xmldir.attributes['path']}"
2722 xmldir.elements.each('dir') { |elem|
2723 if !elem.attributes['deleted']
2724 append_dir_elem.call(child_iter, elem)
2728 append_dir_elem.call(nil, xmldir)
2729 show_password_protections
2731 $albums_tv.expand_all
2733 $albums_tv.selection.select_iter($albums_ts.iter_first)
2737 def select_current_theme
2738 select_theme($xmldoc.root.attributes['theme'],
2739 $xmldoc.root.attributes['limit-sizes'],
2740 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2741 $xmldoc.root.attributes['thumbnails-per-row'])
2744 def open_file(filename)
2748 $current_path = nil #- invalidate
2749 $modified_pixbufs = {}
2752 $subalbums_vb.children.each { |chld|
2753 $subalbums_vb.remove(chld)
2756 if !File.exists?(filename)
2757 return utf8(_("File not found."))
2761 $xmldoc = REXML::Document.new File.new(filename)
2766 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2767 if entry2type(filename).nil?
2768 return utf8(_("Not a booh file!"))
2770 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."))
2774 if !source = $xmldoc.root.attributes['source']
2775 return utf8(_("Corrupted booh file..."))
2778 if !dest = $xmldoc.root.attributes['destination']
2779 return utf8(_("Corrupted booh file..."))
2782 if !theme = $xmldoc.root.attributes['theme']
2783 return utf8(_("Corrupted booh file..."))
2786 if $xmldoc.root.attributes['version'] < '0.8.6'
2787 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2788 mark_document_as_dirty
2789 if $xmldoc.root.attributes['version'] < '0.8.4'
2790 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2791 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2792 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2793 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2794 if old_dest_dir != new_dest_dir
2795 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2797 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2798 xmldir.elements.each { |element|
2799 if %w(image video).include?(element.name) && !element.attributes['deleted']
2800 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2801 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2802 Dir[old_name + '*'].each { |file|
2803 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2804 file != new_file and sys("mv '#{file}' '#{new_file}'")
2807 if element.name == 'dir' && !element.attributes['deleted']
2808 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2809 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2810 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2814 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2818 $xmldoc.root.add_attribute('version', $VERSION)
2821 select_current_theme
2823 $filename = filename
2824 $default_size['thumbnails'] =~ /(.*)x(.*)/
2825 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2826 $albums_thumbnail_size =~ /(.*)x(.*)/
2827 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2829 populate_subalbums_treeview(true)
2831 $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
2835 def open_file_user(filename)
2836 result = open_file(filename)
2838 $config['last-opens'] ||= []
2839 if $config['last-opens'][-1] != utf8(filename)
2840 $config['last-opens'] << utf8(filename)
2842 $orig_filename = $filename
2843 tmp = Tempfile.new("boohtemp")
2846 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2848 $tempfiles << $filename << "#{$filename}.backup"
2850 $orig_filename = nil
2856 if !ask_save_modifications(utf8(_("Save this album?")),
2857 utf8(_("Do you want to save the changes to this album?")),
2858 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2861 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2863 Gtk::FileChooser::ACTION_OPEN,
2865 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2866 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2867 fc.set_current_folder(File.expand_path("~/.booh"))
2868 fc.transient_for = $main_window
2871 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2872 push_mousecursor_wait(fc)
2873 msg = open_file_user(fc.filename)
2888 def additional_booh_options
2891 options += "--mproc #{$config['mproc'].to_i} "
2893 options += "--comments-format '#{$config['comments-format']}'"
2898 if !ask_save_modifications(utf8(_("Save this album?")),
2899 utf8(_("Do you want to save the changes to this album?")),
2900 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2903 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2905 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2906 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2907 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2909 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2910 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2911 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2912 tbl.attach(src = Gtk::Entry.new,
2913 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2914 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2915 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2916 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2917 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2918 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2919 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2920 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2921 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2922 tbl.attach(dest = Gtk::Entry.new,
2923 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2924 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2925 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2926 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2927 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2928 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2929 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2930 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2931 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2933 tooltips = Gtk::Tooltips.new
2934 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2935 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2936 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2937 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2938 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2939 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2940 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)
2941 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2942 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2943 nperpage_model = Gtk::ListStore.new(String, String)
2944 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
2945 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
2946 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
2947 nperpagecombo.set_attributes(crt, { :markup => 0 })
2948 iter = nperpage_model.append
2949 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
2951 [ 12, 20, 30, 40, 50 ].each { |v|
2952 iter = nperpage_model.append
2953 iter[0] = iter[1] = v.to_s
2955 nperpagecombo.active = 0
2956 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
2957 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
2958 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)
2959 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2960 pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
2961 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)
2963 src_nb_calculated_for = ''
2965 process_src_nb = proc {
2966 if src.text != src_nb_calculated_for
2967 src_nb_calculated_for = src.text
2969 Thread.kill(src_nb_thread)
2972 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
2973 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
2975 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2976 if File.readable?(from_utf8_safe(src_nb_calculated_for))
2977 src_nb_thread = Thread.new {
2978 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2979 total = { 'image' => 0, 'video' => 0, nil => 0 }
2980 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2981 if File.basename(dir) =~ /^\./
2985 Dir.entries(dir.chomp).each { |file|
2986 total[entry2type(file)] += 1
2988 rescue Errno::EACCES, Errno::ENOENT
2992 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2996 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2999 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3005 timeout_src_nb = Gtk.timeout_add(100) {
3009 src_browse.signal_connect('clicked') {
3010 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
3012 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3014 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3015 fc.transient_for = $main_window
3016 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3017 src.text = utf8(fc.filename)
3019 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3024 dest_browse.signal_connect('clicked') {
3025 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3027 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3029 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3030 fc.transient_for = $main_window
3031 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3032 dest.text = utf8(fc.filename)
3037 conf_browse.signal_connect('clicked') {
3038 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3040 Gtk::FileChooser::ACTION_SAVE,
3042 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3043 fc.transient_for = $main_window
3044 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3045 fc.set_current_folder(File.expand_path("~/.booh"))
3046 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3047 conf.text = utf8(fc.filename)
3054 recreate_theme_config = proc {
3055 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3057 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3058 $images_size.each { |s|
3059 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3063 tooltips.set_tip(cb, utf8(s['description']), nil)
3064 theme_sizes << { :widget => cb, :value => s['name'] }
3066 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3067 tooltips = Gtk::Tooltips.new
3068 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3069 theme_sizes << { :widget => cb, :value => 'original' }
3072 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3075 $allowed_N_values.each { |n|
3077 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3079 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3081 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3085 nperrows << { :widget => rb, :value => n }
3087 nperrowradios.show_all
3089 recreate_theme_config.call
3091 theme_button.signal_connect('clicked') {
3092 if newtheme = theme_choose(theme_button.label)
3093 theme_button.label = newtheme
3094 recreate_theme_config.call
3098 dialog.vbox.add(frame1)
3099 dialog.vbox.add(frame2)
3105 dialog.run { |response|
3106 if response == Gtk::Dialog::RESPONSE_OK
3107 srcdir = from_utf8_safe(src.text)
3108 destdir = from_utf8_safe(dest.text)
3109 confpath = from_utf8_safe(conf.text)
3110 if src.text != '' && srcdir == ''
3111 show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3113 elsif !File.directory?(srcdir)
3114 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3116 elsif dest.text != '' && destdir == ''
3117 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3119 elsif destdir != make_dest_filename(destdir)
3120 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3122 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3123 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
3125 elsif File.exists?(destdir) && !File.directory?(destdir)
3126 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3128 elsif conf.text == ''
3129 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3131 elsif conf.text != '' && confpath == ''
3132 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3134 elsif File.directory?(confpath)
3135 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3137 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3138 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3140 system("mkdir '#{destdir}'")
3141 if !File.directory?(destdir)
3142 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3154 srcdir = from_utf8(src.text)
3155 destdir = from_utf8(dest.text)
3156 configskel = File.expand_path(from_utf8(conf.text))
3157 theme = theme_button.label
3158 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3159 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3160 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3161 opt432 = optimize432.active?
3162 madewith = madewithentry.text.gsub('"', '"').gsub('\'', ''')
3163 indexlink = indexlinkentry.text.gsub('"', '"').gsub('\'', ''')
3166 Thread.kill(src_nb_thread)
3167 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3170 Gtk.timeout_remove(timeout_src_nb)
3173 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3174 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3175 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3176 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3177 utf8(_("Please wait while scanning source directory...")),
3179 { :closure_after => proc { open_file_user(configskel) } })
3184 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3186 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3187 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3188 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3190 source = $xmldoc.root.attributes['source']
3191 dest = $xmldoc.root.attributes['destination']
3192 theme = $xmldoc.root.attributes['theme']
3193 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3194 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3195 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3196 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3198 limit_sizes = limit_sizes.split(/,/)
3200 madewith = $xmldoc.root.attributes['made-with'].gsub(''', '\'')
3201 indexlink = $xmldoc.root.attributes['index-link'].gsub(''', '\'')
3203 tooltips = Gtk::Tooltips.new
3204 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3205 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3206 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3207 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3208 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3209 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3210 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3211 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3212 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3213 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3214 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3215 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3216 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3218 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3219 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3220 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3221 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3222 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3223 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3224 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)
3225 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3226 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3227 nperpage_model = Gtk::ListStore.new(String, String)
3228 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3229 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3230 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3231 nperpagecombo.set_attributes(crt, { :markup => 0 })
3232 iter = nperpage_model.append
3233 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3235 [ 12, 20, 30, 40, 50 ].each { |v|
3236 iter = nperpage_model.append
3237 iter[0] = iter[1] = v.to_s
3238 if nperpage && nperpage == v.to_s
3239 nperpagecombo.active_iter = iter
3242 if nperpagecombo.active_iter.nil?
3243 nperpagecombo.active = 0
3246 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3247 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3249 indexlinkentry.text = indexlink
3251 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)
3252 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3253 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3255 madewithentry.text = madewith
3257 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)
3261 recreate_theme_config = proc {
3262 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3264 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3266 $images_size.each { |s|
3267 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3269 if limit_sizes.include?(s['name'])
3277 tooltips.set_tip(cb, utf8(s['description']), nil)
3278 theme_sizes << { :widget => cb, :value => s['name'] }
3280 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3281 tooltips = Gtk::Tooltips.new
3282 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3283 if limit_sizes && limit_sizes.include?('original')
3286 theme_sizes << { :widget => cb, :value => 'original' }
3289 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3292 $allowed_N_values.each { |n|
3294 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3296 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3298 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3299 nperrowradios.add(Gtk::Label.new(' '))
3300 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3303 nperrows << { :widget => rb, :value => n.to_s }
3305 nperrowradios.show_all
3307 recreate_theme_config.call
3309 theme_button.signal_connect('clicked') {
3310 if newtheme = theme_choose(theme_button.label)
3313 theme_button.label = newtheme
3314 recreate_theme_config.call
3318 dialog.vbox.add(frame1)
3319 dialog.vbox.add(frame2)
3325 dialog.run { |response|
3326 if response == Gtk::Dialog::RESPONSE_OK
3327 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3328 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3337 save_theme = theme_button.label
3338 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3339 save_opt432 = optimize432.active?
3340 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3341 save_nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3342 save_madewith = madewithentry.text.gsub('"', '"').gsub('\'', ''')
3343 save_indexlink = indexlinkentry.text.gsub('"', '"').gsub('\'', ''')
3346 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_nperpage != nperpage || save_madewith != madewith || save_indexlink != indexlinkentry)
3347 mark_document_as_dirty
3349 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3350 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3351 (save_nperpage ? "--thumbnails-per-page #{save_nperpage} " : '') +
3352 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3353 utf8(_("Please wait while scanning source directory...")),
3355 { :closure_after => proc {
3356 open_file($filename)
3360 #- select_theme merges global variables, need to return to current choices
3361 select_current_theme
3368 sel = $albums_tv.selection.selected_rows
3370 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3371 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3372 utf8(_("Please wait while scanning source directory...")),
3374 { :closure_after => proc {
3375 open_file($filename)
3376 $albums_tv.selection.select_path(sel[0])
3384 sel = $albums_tv.selection.selected_rows
3386 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3387 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3388 utf8(_("Please wait while scanning source directory...")),
3390 { :closure_after => proc {
3391 open_file($filename)
3392 $albums_tv.selection.select_path(sel[0])
3400 theme = $xmldoc.root.attributes['theme']
3401 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3403 limit_sizes = "--sizes #{limit_sizes}"
3405 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3406 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3407 utf8(_("Please wait while scanning source directory...")),
3409 { :closure_after => proc {
3410 open_file($filename)
3416 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3418 Gtk::FileChooser::ACTION_SAVE,
3420 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3421 fc.transient_for = $main_window
3422 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3423 fc.set_current_folder(File.expand_path("~/.booh"))
3424 fc.filename = $orig_filename
3425 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3426 $orig_filename = fc.filename
3427 if ! save_current_file_user
3431 $config['last-opens'] ||= []
3432 $config['last-opens'] << $orig_filename
3438 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3440 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3441 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3442 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3444 dialog.vbox.add(notebook = Gtk::Notebook.new)
3445 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3446 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3447 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3448 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)),
3449 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3450 tooltips = Gtk::Tooltips.new
3451 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3452 for example: /usr/bin/mplayer %f")), nil)
3453 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3454 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3455 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3456 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3457 tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3458 for example: /usr/bin/gimp-remote %f")), nil)
3459 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3460 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3461 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3462 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3463 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3464 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3465 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3466 0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3467 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)),
3468 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3469 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)
3470 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3471 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3472 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)
3473 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3474 0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3475 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)
3477 smp_check.signal_connect('toggled') {
3478 if smp_check.active?
3479 smp_hbox.sensitive = true
3481 smp_hbox.sensitive = false
3485 smp_check.active = true
3486 smp_spin.value = $config['mproc'].to_i
3488 nogestures_check.active = $config['nogestures']
3489 deleteondisk_check.active = $config['deleteondisk']
3491 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3492 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3493 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3494 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3495 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3496 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3497 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3498 tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3499 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3500 tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3501 2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3502 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)
3503 commentsformat_help.signal_connect('clicked') {
3504 show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3505 hence you should look at ImageMagick/identify documentation for the most
3506 accurate and up-to-date documentation. Last time I checked, documentation
3509 Print information about the image in a format of your choosing. You can
3510 include the image filename, type, width, height, Exif data, or other image
3511 attributes by embedding special format characters:
3514 %P page width and height
3518 %e filename extension
3523 %k number of unique colors
3530 %r image class and colorspace
3533 %u unique temporary filename
3546 displays MIFF:bird.miff 512x480 for an image titled bird.miff and whose
3547 width is 512 and height is 480.
3549 If the first character of string is @, the format is read from a file titled
3550 by the remaining characters in the string.
3552 You can also use the following special formatting syntax to print Exif
3553 information contained in the file:
3557 Where tag can be one of the following:
3559 * (print all Exif tags, in keyword=data format)
3560 ! (print all Exif tags, in tag_number data format)
3561 #hhhh (print data for Exif tag #hhhh)
3566 PhotometricInterpretation
3586 PrimaryChromaticities
3589 JPEGInterchangeFormat
3590 JPEGInterchangeFormatLength
3612 ComponentsConfiguration
3613 CompressedBitsPerPixel
3633 InteroperabilityOffset
3635 SpatialFrequencyResponse
3636 FocalPlaneXResolution
3637 FocalPlaneYResolution
3638 FocalPlaneResolutionUnit
3643 SceneType")), { :scrolled => true })
3646 tbl.attach(update_exif_orientation_check = Gtk::CheckButton.new(utf8(_("Update file's EXIF orientation when rotating a picture"))),
3647 0, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3648 tooltips.set_tip(update_exif_orientation_check, utf8(_("When rotating a picture (Alt-Right/Left), also update EXIF orientation in the file itself")), nil)
3649 update_exif_orientation_check.active = $config['rotate-set-exif'] == 'true'
3651 dialog.vbox.show_all
3652 dialog.run { |response|
3653 if response == Gtk::Dialog::RESPONSE_OK
3654 $config['video-viewer'] = from_utf8(video_viewer_entry.text)
3655 $config['image-editor'] = from_utf8(image_editor_entry.text)
3656 $config['browser'] = from_utf8(browser_entry.text)
3657 if smp_check.active?
3658 $config['mproc'] = smp_spin.value.to_i
3660 $config.delete('mproc')
3662 $config['nogestures'] = nogestures_check.active?
3663 $config['deleteondisk'] = deleteondisk_check.active?
3665 $config['convert-enhance'] = from_utf8(enhance_entry.text)
3666 $config['comments-format'] = from_utf8(commentsformat_entry.text.gsub(/'/, ''))
3667 $config['rotate-set-exif'] = update_exif_orientation_check.active?.to_s
3674 if $undo_tb.sensitive?
3675 $redo_tb.sensitive = $redo_mb.sensitive = true
3676 if not more_undoes = UndoHandler.undo($statusbar)
3677 $undo_tb.sensitive = $undo_mb.sensitive = false
3683 if $redo_tb.sensitive?
3684 $undo_tb.sensitive = $undo_mb.sensitive = true
3685 if not more_redoes = UndoHandler.redo($statusbar)
3686 $redo_tb.sensitive = $redo_mb.sensitive = false
3691 def show_one_click_explanation(intro)
3692 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
3694 %s When such a tool is activated
3695 (<span foreground='darkblue'>Rotate clockwise</span>, <span foreground='darkblue'>Rotate counter-clockwise</span>, <span foreground='darkblue'>Enhance</span> or <span foreground='darkblue'>Delete</span>), clicking
3696 on a thumbnail will immediately apply the desired action.
3698 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
3699 ") % intro), { :pos_centered => true })
3702 def create_menu_and_toolbar
3705 mb = Gtk::MenuBar.new
3707 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
3708 filesubmenu = Gtk::Menu.new
3709 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
3710 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
3711 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3712 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
3713 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
3714 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3715 tooltips = Gtk::Tooltips.new
3716 filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
3717 $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3718 tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
3719 filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
3720 $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3721 tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
3722 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
3723 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3724 tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums (anywhere)")), nil)
3725 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3726 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
3727 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3728 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
3729 filesubmenu.append($view_wa = Gtk::ImageMenuItem.new(utf8(_("View web-album with browser"))).set_sensitive(false))
3730 $view_wa.image = Gtk::Image.new("#{$FPATH}/images/stock-view-webalbum-16.png")
3731 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3732 filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
3733 tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
3734 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3735 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
3736 filemenu.set_submenu(filesubmenu)
3739 new.signal_connect('activate') { new_album }
3740 open.signal_connect('activate') { open_file_popup }
3741 $save.signal_connect('activate') { save_current_file_user }
3742 $save_as.signal_connect('activate') { save_as_do }
3743 $merge_current.signal_connect('activate') { merge_current }
3744 $merge_newsubs.signal_connect('activate') { merge_newsubs }
3745 $merge.signal_connect('activate') { merge }
3746 $generate.signal_connect('activate') {
3748 call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
3749 utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
3751 { :successmsg => utf8(_("Your web-album is now ready in directory '%s'.
3752 Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
3753 :successmsg_linkurl => $xmldoc.root.attributes['destination'],
3754 :closure_after => proc {
3755 $xmldoc.elements.each('//dir') { |elem|
3756 $modified ||= elem.attributes['already-generated'].nil?
3757 elem.add_attribute('already-generated', 'true')
3759 UndoHandler.cleanup #- prevent save_changes to mark current dir as not already generated
3760 $undo_tb.sensitive = $undo_mb.sensitive = false
3761 $redo_tb.sensitive = $redo_mb.sensitive = false
3763 $generated_outofline = true
3766 $view_wa.signal_connect('activate') {
3767 indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
3768 if File.exists?(indexhtml)
3771 show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
3774 $properties.signal_connect('activate') { properties }
3776 quit.signal_connect('activate') { try_quit }
3778 editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
3779 editsubmenu = Gtk::Menu.new
3780 editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
3781 editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
3782 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3783 editsubmenu.append($sort_by_exif_date = Gtk::ImageMenuItem.new(utf8(_("Sort by EXIF date"))).set_sensitive(false))
3784 $sort_by_exif_date.image = Gtk::Image.new("#{$FPATH}/images/sort_by_exif_date.png")
3785 editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
3786 $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
3787 tooltips.set_tip($remove_all_captions, utf8(_("Mainly useful when you don't want to type any caption, that will remove default captions made of filenames")), nil)
3788 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3789 editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
3790 editmenu.set_submenu(editsubmenu)
3793 $remove_all_captions.signal_connect('activate') { remove_all_captions }
3794 $sort_by_exif_date.signal_connect('activate') { sort_by_exif_date }
3796 prefs.signal_connect('activate') { preferences }
3798 helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
3799 helpsubmenu = Gtk::Menu.new
3800 helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
3801 one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
3802 helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
3803 speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
3804 helpsubmenu.append(tutos = Gtk::ImageMenuItem.new(utf8(_("Online tutorials (opens a web-browser)"))))
3805 tutos.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3806 helpsubmenu.append(Gtk::SeparatorMenuItem.new)
3807 helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
3808 helpmenu.set_submenu(helpsubmenu)
3811 one_click.signal_connect('activate') {
3812 show_one_click_explanation(_("One-Click tools are available in the toolbar."))
3815 speed.signal_connect('activate') {
3816 show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
3818 <span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
3819 <span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
3820 <span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
3821 <span foreground='darkblue'>Control-Enter</span>: for an image, open larger view; for a video, launch player
3822 <span foreground='darkblue'>Control-Delete</span>: delete image
3823 <span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
3824 <span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
3825 <span foreground='darkblue'>Control-z</span>: undo
3826 <span foreground='darkblue'>Control-r</span>: redo
3828 <span size='large' weight='bold'>Mouse gestures:</span>
3830 Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
3831 for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
3833 <span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
3834 <span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
3835 <span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
3836 <span foreground='darkblue'>Left click, hold left button, right click</span>: undo
3837 <span foreground='darkblue'>Right click, hold right button, left click</span>: redo
3838 ")), { :pos_centered => true, :not_transient => true })
3841 tutos.signal_connect('activate') {
3842 open_url('http://booh.org/tutorial.html')
3845 about.signal_connect('activate') { call_about }
3849 tb = Gtk::Toolbar.new
3851 tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
3852 open.label = utf8(_("Open")) #- to avoid missing gtk2 l10n catalogs
3853 open.menu = Gtk::Menu.new
3854 open.signal_connect('clicked') { open_file_popup }
3855 open.signal_connect('show-menu') {
3856 lastopens = Gtk::Menu.new
3858 if $config['last-opens']
3859 $config['last-opens'].reverse.each { |e|
3860 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
3861 item.signal_connect('activate') {
3862 if ask_save_modifications(utf8(_("Save this album?")),
3863 utf8(_("Do you want to save the changes to this album?")),
3864 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3865 push_mousecursor_wait
3866 msg = open_file_user(from_utf8(e))
3869 show_popup($main_window, msg)
3877 open.menu = lastopens
3880 tb.insert(-1, Gtk::SeparatorToolItem.new)
3882 tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
3883 $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
3884 $r90.label = utf8(_("Rotate"))
3885 tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
3886 $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
3887 $r270.label = utf8(_("Rotate"))
3888 tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
3889 $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
3890 $enhance.label = utf8(_("Enhance"))
3891 tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
3892 $delete.label = utf8(_("Delete")) #- to avoid missing gtk2 l10n catalogs
3893 tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
3894 nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
3895 nothing.label = utf8(_("None"))
3897 tb.insert(-1, Gtk::SeparatorToolItem.new)
3899 tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
3900 tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
3903 $undo_tb.signal_connect('clicked') { perform_undo }
3904 $undo_mb.signal_connect('activate') { perform_undo }
3905 $redo_tb.signal_connect('clicked') { perform_redo }
3906 $redo_mb.signal_connect('activate') { perform_redo }
3908 one_click_explain_try = proc {
3909 if !$config['one-click-explained']
3910 show_one_click_explanation(_("You have just clicked on a One-Click tool."))
3911 $config['one-click-explained'] = true
3915 $r90.signal_connect('toggled') {
3917 set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
3918 one_click_explain_try.call
3919 $r270.active = false
3920 $enhance.active = false
3921 $delete.active = false
3922 nothing.sensitive = true
3924 if !$r270.active? && !$enhance.active? && !$delete.active?
3925 set_mousecursor_normal
3926 nothing.sensitive = false
3928 nothing.sensitive = true
3933 $r270.signal_connect('toggled') {
3935 set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
3936 one_click_explain_try.call
3938 $enhance.active = false
3939 $delete.active = false
3940 nothing.sensitive = true
3942 if !$r90.active? && !$enhance.active? && !$delete.active?
3943 set_mousecursor_normal
3944 nothing.sensitive = false
3946 nothing.sensitive = true
3950 $enhance.signal_connect('toggled') {
3952 set_mousecursor(Gdk::Cursor::SPRAYCAN)
3953 one_click_explain_try.call
3955 $r270.active = false
3956 $delete.active = false
3957 nothing.sensitive = true
3959 if !$r90.active? && !$r270.active? && !$delete.active?
3960 set_mousecursor_normal
3961 nothing.sensitive = false
3963 nothing.sensitive = true
3967 $delete.signal_connect('toggled') {
3969 set_mousecursor(Gdk::Cursor::PIRATE)
3970 one_click_explain_try.call
3972 $r270.active = false
3973 $enhance.active = false
3974 nothing.sensitive = true
3976 if !$r90.active? && !$r270.active? && !$enhance.active?
3977 set_mousecursor_normal
3978 nothing.sensitive = false
3980 nothing.sensitive = true