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-2008 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 'booh/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|
212 def set_mousecursor(what, *widget)
213 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
214 if widget[0] && widget[0].window
215 widget[0].window.cursor = cursor
217 if $main_window && $main_window.window
218 $main_window.window.cursor = cursor
220 $current_cursor = what
222 def set_mousecursor_wait(*widget)
223 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
224 if Thread.current == Thread.main
225 Gtk.main_iteration while Gtk.events_pending?
228 def set_mousecursor_normal(*widget)
229 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
231 def push_mousecursor_wait(*widget)
232 if $current_cursor != Gdk::Cursor::WATCH
233 $save_cursor = $current_cursor
234 gtk_thread_protect { set_mousecursor_wait(*widget) }
237 def pop_mousecursor(*widget)
238 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
242 source = $xmldoc.root.attributes['source']
243 dest = $xmldoc.root.attributes['destination']
244 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
247 def full_src_dir_to_rel(path, source)
248 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
251 def build_full_dest_filename(filename)
252 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
255 def save_undo(name, closure, *params)
256 UndoHandler.save_undo(name, closure, [ *params ])
257 $undo_tb.sensitive = $undo_mb.sensitive = true
258 $redo_tb.sensitive = $redo_mb.sensitive = false
261 def view_element(filename, closures)
262 if entry2type(filename) == 'video'
263 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
269 w = Gtk::Window.new.set_title(filename)
271 msg 3, "filename: #{filename}"
272 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
273 #- typically this file won't exist in case of videos; try with the largest thumbnail around
274 if !File.exists?(dest_img)
275 if entry2type(filename) == 'video'
276 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
277 if not alternatives.empty?
278 dest_img = alternatives[-1]
281 push_mousecursor_wait
282 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
284 if !File.exists?(dest_img)
285 msg 2, _("Could not generate fullscreen thumbnail!")
290 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)))
291 evt.signal_connect('button-press-event') { |this, event|
292 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
293 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
295 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
297 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
298 delete_item.signal_connect('activate') {
300 closures[:delete].call(false)
303 menu.popup(nil, nil, event.button, event.time)
306 evt.signal_connect('button-release-event') { |this, event|
308 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
309 msg 3, "gesture delete: click-drag right button to the bottom"
311 closures[:delete].call(false)
312 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
316 tooltips = Gtk::Tooltips.new
317 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
319 w.signal_connect('key-press-event') { |w,event|
320 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
322 closures[:delete].call(false)
326 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
327 b.signal_connect('clicked') { w.destroy }
330 vb.pack_start(evt, false, false)
331 vb.pack_end(bottom, false, false)
334 w.signal_connect('delete-event') { w.destroy }
335 w.window_position = Gtk::Window::POS_CENTER
339 def scroll_upper(scrolledwindow, ypos_top)
340 newval = scrolledwindow.vadjustment.value -
341 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
342 if newval < scrolledwindow.vadjustment.lower
343 newval = scrolledwindow.vadjustment.lower
345 scrolledwindow.vadjustment.value = newval
348 def scroll_lower(scrolledwindow, ypos_bottom)
349 newval = scrolledwindow.vadjustment.value +
350 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
351 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
352 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
354 scrolledwindow.vadjustment.value = newval
357 def autoscroll_if_needed(scrolledwindow, image, textview)
358 #- autoscroll if cursor or image is not visible, if possible
359 if image && image.window || textview.window
360 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
361 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
362 current_miny_visible = scrolledwindow.vadjustment.value
363 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
364 if ypos_top < current_miny_visible
365 scroll_upper(scrolledwindow, ypos_top)
366 elsif ypos_bottom > current_maxy_visible
367 scroll_lower(scrolledwindow, ypos_bottom)
372 def create_editzone(scrolledwindow, pagenum, image)
373 frame = Gtk::Frame.new
374 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
375 frame.set_shadow_type(Gtk::SHADOW_IN)
376 textview.signal_connect('key-press-event') { |w, event|
377 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
378 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
379 scrolledwindow.signal_emit('key-press-event', event)
381 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
382 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
383 if event.keyval == Gdk::Keyval::GDK_Up
384 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
385 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
387 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
390 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
391 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
393 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
400 candidate_undo_text = nil
401 textview.signal_connect('focus-in-event') { |w, event|
402 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
403 candidate_undo_text = textview.buffer.text
407 textview.signal_connect('key-release-event') { |w, event|
408 if candidate_undo_text && candidate_undo_text != textview.buffer.text
410 save_undo(_("text edit"),
412 save_text = textview.buffer.text
413 textview.buffer.text = text
415 $notebook.set_page(pagenum)
417 textview.buffer.text = save_text
419 $notebook.set_page(pagenum)
421 }, candidate_undo_text)
422 candidate_undo_text = nil
425 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)
426 autoscroll_if_needed(scrolledwindow, image, textview)
431 return [ frame, textview ]
434 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
436 if !$modified_pixbufs[thumbnail_img]
437 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
438 elsif !$modified_pixbufs[thumbnail_img][:orig]
439 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
442 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
445 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
446 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
447 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
448 if pixbuf.height > desired_y
449 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
450 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
451 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
456 if $modified_pixbufs[thumbnail_img][:whitebalance]
457 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
460 #- fix gamma correction
461 if $modified_pixbufs[thumbnail_img][:gammacorrect]
462 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
465 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
468 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
471 #- update rotate attribute
472 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
473 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
475 if $config['rotate-set-exif'] == 'true'
476 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
479 $modified_pixbufs[thumbnail_img] ||= {}
480 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
481 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
483 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
486 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
489 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
491 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
493 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
494 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
496 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
497 $notebook.set_page(0)
498 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
503 def color_swap(xmldir, attributes_prefix)
505 if xmldir.attributes["#{attributes_prefix}color-swap"]
506 xmldir.delete_attribute("#{attributes_prefix}color-swap")
508 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
512 def enhance(xmldir, attributes_prefix)
514 if xmldir.attributes["#{attributes_prefix}enhance"]
515 xmldir.delete_attribute("#{attributes_prefix}enhance")
517 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
521 def change_seektime(xmldir, attributes_prefix, value)
523 xmldir.add_attribute("#{attributes_prefix}seektime", value)
526 def ask_new_seektime(xmldir, attributes_prefix)
528 value = xmldir.attributes["#{attributes_prefix}seektime"]
533 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
535 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
536 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
537 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
541 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
545 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
546 entry.signal_connect('key-press-event') { |w, event|
547 if event.keyval == Gdk::Keyval::GDK_Return
548 dialog.response(Gtk::Dialog::RESPONSE_OK)
550 elsif event.keyval == Gdk::Keyval::GDK_Escape
551 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
554 false #- propagate if needed
558 dialog.window_position = Gtk::Window::POS_MOUSE
561 dialog.run { |response|
564 if response == Gtk::Dialog::RESPONSE_OK
566 msg 3, "changing seektime to #{newval}"
567 return { :old => value, :new => newval }
574 def change_pano_amount(xmldir, attributes_prefix, value)
577 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
579 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
583 def ask_new_pano_amount(xmldir, attributes_prefix)
585 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
590 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
592 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
593 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
594 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
598 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
599 of this panorama image compared to other regular images. For example, if the panorama
600 was taken out of four photos on one row, counting the necessary overlap, the width of
601 this panorama image should probably be roughly three times the width of regular images.
603 With this information, booh will be able to generate panorama thumbnails looking
607 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)")))).
608 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
609 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
610 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
611 spin.signal_connect('value-changed') {
614 dialog.window_position = Gtk::Window::POS_MOUSE
617 spin.value = value.to_f
624 dialog.run { |response|
628 newval = spin.value.to_f
631 if response == Gtk::Dialog::RESPONSE_OK
633 msg 3, "changing panorama amount to #{newval}"
634 return { :old => value, :new => newval }
641 def change_whitebalance(xmlelem, attributes_prefix, value)
643 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
646 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
648 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
649 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
650 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
651 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
652 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
653 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
654 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
655 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
656 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
657 $modified_pixbufs[thumbnail_img] ||= {}
658 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
659 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
661 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
662 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
664 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
667 $modified_pixbufs[thumbnail_img] ||= {}
668 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
670 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
673 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
674 #- init $modified_pixbufs correctly
675 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
677 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
679 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
681 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
682 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
683 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
687 _("You can fix the <b>white balance</b> of the image, if your image is too blue
688 or too yellow because your camera didn't detect the light correctly. Drag the
689 slider below the image to the left for more blue, to the right for more yellow.
693 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
695 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
697 dialog.window_position = Gtk::Window::POS_MOUSE
701 timeout = Gtk.timeout_add(100) {
702 if hs.value != lastval
705 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
711 dialog.run { |response|
712 Gtk.timeout_remove(timeout)
713 if response == Gtk::Dialog::RESPONSE_OK
715 newval = hs.value.to_s
716 msg 3, "changing white balance to #{newval}"
718 return { :old => value, :new => newval }
721 $modified_pixbufs[thumbnail_img] ||= {}
722 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
723 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
731 def change_gammacorrect(xmlelem, attributes_prefix, value)
733 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
736 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
738 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
739 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
740 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
741 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
742 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
743 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
744 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
745 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
746 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
747 $modified_pixbufs[thumbnail_img] ||= {}
748 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
749 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
751 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
752 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
754 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
757 $modified_pixbufs[thumbnail_img] ||= {}
758 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
760 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
763 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
764 #- init $modified_pixbufs correctly
765 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
767 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
769 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
771 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
772 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
773 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
777 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
778 or too bright. Drag the slider below the image.
782 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
784 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
786 dialog.window_position = Gtk::Window::POS_MOUSE
790 timeout = Gtk.timeout_add(100) {
791 if hs.value != lastval
794 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
800 dialog.run { |response|
801 Gtk.timeout_remove(timeout)
802 if response == Gtk::Dialog::RESPONSE_OK
804 newval = hs.value.to_s
805 msg 3, "gamma correction to #{newval}"
807 return { :old => value, :new => newval }
810 $modified_pixbufs[thumbnail_img] ||= {}
811 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
812 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
820 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
821 if File.exists?(destfile)
822 File.delete(destfile)
824 #- type can be 'element' or 'subdir'
826 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
828 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
832 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
834 push_mousecursor_wait
835 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
838 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
844 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
845 distribute_multiple_call = Proc.new { |action, arg|
846 $selected_elements.each_key { |path|
847 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
849 if possible_actions[:can_multiple] && $selected_elements.length > 0
850 UndoHandler.begin_batch
851 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
852 UndoHandler.end_batch
854 closures[action].call(arg)
856 $selected_elements = {}
859 if optionals.include?('change_image')
860 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
861 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
862 changeimg.signal_connect('activate') { closures[:change].call }
863 menu.append(Gtk::SeparatorMenuItem.new)
865 if !possible_actions[:can_multiple] || $selected_elements.length == 0
868 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
869 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
870 view.signal_connect('activate') { closures[:view].call }
872 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
873 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
874 view.signal_connect('activate') { closures[:view].call }
875 menu.append(Gtk::SeparatorMenuItem.new)
878 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
879 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
880 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
881 exif.signal_connect('activate') { show_popup($main_window,
882 utf8(`exif -m '#{fullpath}'`),
883 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
884 menu.append(Gtk::SeparatorMenuItem.new)
887 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
888 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
889 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
890 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
891 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
892 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
893 if !possible_actions[:can_multiple] || $selected_elements.length == 0
894 menu.append(Gtk::SeparatorMenuItem.new)
895 if !possible_actions[:forbid_left]
896 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
897 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
898 moveleft.signal_connect('activate') { closures[:move].call('left') }
899 if !possible_actions[:can_left]
900 moveleft.sensitive = false
903 if !possible_actions[:forbid_right]
904 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
905 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
906 moveright.signal_connect('activate') { closures[:move].call('right') }
907 if !possible_actions[:can_right]
908 moveright.sensitive = false
911 if optionals.include?('move_top')
912 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
913 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
914 movetop.signal_connect('activate') { closures[:move].call('top') }
915 if !possible_actions[:can_top]
916 movetop.sensitive = false
919 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
920 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
921 moveup.signal_connect('activate') { closures[:move].call('up') }
922 if !possible_actions[:can_up]
923 moveup.sensitive = false
925 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
926 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
927 movedown.signal_connect('activate') { closures[:move].call('down') }
928 if !possible_actions[:can_down]
929 movedown.sensitive = false
931 if optionals.include?('move_bottom')
932 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
933 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
934 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
935 if !possible_actions[:can_bottom]
936 movebottom.sensitive = false
941 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
942 menu.append(Gtk::SeparatorMenuItem.new)
943 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
944 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
945 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
946 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
947 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
948 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
949 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
950 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
951 seektime.signal_connect('activate') {
952 if possible_actions[:can_multiple] && $selected_elements.length > 0
953 if values = ask_new_seektime(nil, '')
954 distribute_multiple_call.call(:seektime, values)
957 closures[:seektime].call
962 menu.append( Gtk::SeparatorMenuItem.new)
963 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
964 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
965 gammacorrect.signal_connect('activate') {
966 if possible_actions[:can_multiple] && $selected_elements.length > 0
967 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
968 distribute_multiple_call.call(:gammacorrect, values)
971 closures[:gammacorrect].call
974 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
975 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
976 whitebalance.signal_connect('activate') {
977 if possible_actions[:can_multiple] && $selected_elements.length > 0
978 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
979 distribute_multiple_call.call(:whitebalance, values)
982 closures[:whitebalance].call
985 if !possible_actions[:can_multiple] || $selected_elements.length == 0
986 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
987 _("Enhance constrast"))))
989 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
991 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
992 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
993 if type == 'image' && possible_actions[:can_panorama]
994 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
995 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
996 panorama.signal_connect('activate') {
997 if possible_actions[:can_multiple] && $selected_elements.length > 0
998 if values = ask_new_pano_amount(nil, '')
999 distribute_multiple_call.call(:pano, values)
1002 distribute_multiple_call.call(:pano)
1006 menu.append( Gtk::SeparatorMenuItem.new)
1007 if optionals.include?('delete')
1008 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1009 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1010 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1011 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1012 paste_item.signal_connect('activate') { closures[:paste].call }
1013 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1014 clear_item.signal_connect('activate') { $cuts = [] }
1016 paste_item.sensitive = clear_item.sensitive = false
1019 menu.append( Gtk::SeparatorMenuItem.new)
1021 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1022 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1023 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1024 editexternally.signal_connect('activate') {
1025 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1030 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1031 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1032 if optionals.include?('delete')
1033 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1034 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1037 menu.popup(nil, nil, event.button, event.time)
1040 def delete_current_subalbum
1042 sel = $albums_tv.selection.selected_rows
1043 $xmldir.elements.each { |e|
1044 if e.name == 'image' || e.name == 'video'
1045 e.add_attribute('deleted', 'true')
1048 #- branch if we have a non deleted subalbum
1049 if $xmldir.child_byname_notattr('dir', 'deleted')
1050 $xmldir.delete_attribute('thumbnails-caption')
1051 $xmldir.delete_attribute('thumbnails-captionfile')
1053 $xmldir.add_attribute('deleted', 'true')
1055 while moveup.parent.name == 'dir'
1056 moveup = moveup.parent
1057 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1058 moveup.add_attribute('deleted', 'true')
1065 save_changes('forced')
1066 populate_subalbums_treeview(false)
1067 $albums_tv.selection.select_path(sel[0])
1073 $current_path = nil #- prevent save_changes from being rerun again
1074 sel = $albums_tv.selection.selected_rows
1075 restore_one = proc { |xmldir|
1076 xmldir.elements.each { |e|
1077 if e.name == 'dir' && e.attributes['deleted']
1080 e.delete_attribute('deleted')
1083 restore_one.call($xmldir)
1084 populate_subalbums_treeview(false)
1085 $albums_tv.selection.select_path(sel[0])
1088 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1091 frame1 = Gtk::Frame.new
1092 fullpath = from_utf8("#{$current_path}/#{filename}")
1094 my_gen_real_thumbnail = proc {
1095 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1099 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1100 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1101 pack_start(img = Gtk::Image.new).
1102 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1103 px, mask = pxb.render_pixmap_and_mask(0)
1104 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1105 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1107 frame1.add(img = Gtk::Image.new)
1110 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1111 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1112 my_gen_real_thumbnail.call
1114 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1117 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1119 tooltips = Gtk::Tooltips.new
1120 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1121 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1123 frame2, textview = create_editzone($autotable_sw, 1, img)
1124 textview.buffer.text = caption
1125 textview.set_justification(Gtk::Justification::CENTER)
1127 vbox = Gtk::VBox.new(false, 5)
1128 vbox.pack_start(evtbox, false, false)
1129 vbox.pack_start(frame2, false, false)
1130 autotable.append(vbox, filename)
1132 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1133 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1135 #- to be able to find widgets by name
1136 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1138 cleanup_all_thumbnails = proc {
1139 #- remove out of sync images
1140 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1141 for sizeobj in $images_size
1142 for file in [ "#{dest_img_base}-#{sizeobj['fullscreen']}.jpg", "#{dest_img_base}-#{sizeobj['thumbnails']}.jpg" ]
1143 if File.exists?(file)
1152 cleanup_all_thumbnails.call
1153 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1155 $xmldir.delete_attribute('already-generated')
1156 my_gen_real_thumbnail.call
1159 rotate_and_cleanup = proc { |angle|
1160 cleanup_all_thumbnails.call
1161 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1164 move = proc { |direction|
1165 do_method = "move_#{direction}"
1166 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1168 done = autotable.method(do_method).call(vbox)
1169 textview.grab_focus #- because if moving, focus is stolen
1173 save_undo(_("move %s") % direction,
1175 autotable.method(undo_method).call(vbox)
1176 textview.grab_focus #- because if moving, focus is stolen
1177 autoscroll_if_needed($autotable_sw, img, textview)
1178 $notebook.set_page(1)
1180 autotable.method(do_method).call(vbox)
1181 textview.grab_focus #- because if moving, focus is stolen
1182 autoscroll_if_needed($autotable_sw, img, textview)
1183 $notebook.set_page(1)
1189 color_swap_and_cleanup = proc {
1190 perform_color_swap_and_cleanup = proc {
1191 cleanup_all_thumbnails.call
1192 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1193 my_gen_real_thumbnail.call
1196 perform_color_swap_and_cleanup.call
1198 save_undo(_("color swap"),
1200 perform_color_swap_and_cleanup.call
1202 autoscroll_if_needed($autotable_sw, img, textview)
1203 $notebook.set_page(1)
1205 perform_color_swap_and_cleanup.call
1207 autoscroll_if_needed($autotable_sw, img, textview)
1208 $notebook.set_page(1)
1213 change_seektime_and_cleanup_real = proc { |values|
1214 perform_change_seektime_and_cleanup = proc { |val|
1215 cleanup_all_thumbnails.call
1216 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1217 my_gen_real_thumbnail.call
1219 perform_change_seektime_and_cleanup.call(values[:new])
1221 save_undo(_("specify seektime"),
1223 perform_change_seektime_and_cleanup.call(values[:old])
1225 autoscroll_if_needed($autotable_sw, img, textview)
1226 $notebook.set_page(1)
1228 perform_change_seektime_and_cleanup.call(values[:new])
1230 autoscroll_if_needed($autotable_sw, img, textview)
1231 $notebook.set_page(1)
1236 change_seektime_and_cleanup = proc {
1237 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1238 change_seektime_and_cleanup_real.call(values)
1242 change_pano_amount_and_cleanup_real = proc { |values|
1243 perform_change_pano_amount_and_cleanup = proc { |val|
1244 cleanup_all_thumbnails.call
1245 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1247 perform_change_pano_amount_and_cleanup.call(values[:new])
1249 save_undo(_("change panorama amount"),
1251 perform_change_pano_amount_and_cleanup.call(values[:old])
1253 autoscroll_if_needed($autotable_sw, img, textview)
1254 $notebook.set_page(1)
1256 perform_change_pano_amount_and_cleanup.call(values[:new])
1258 autoscroll_if_needed($autotable_sw, img, textview)
1259 $notebook.set_page(1)
1264 change_pano_amount_and_cleanup = proc {
1265 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1266 change_pano_amount_and_cleanup_real.call(values)
1270 whitebalance_and_cleanup_real = proc { |values|
1271 perform_change_whitebalance_and_cleanup = proc { |val|
1272 cleanup_all_thumbnails.call
1273 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1274 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1275 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1277 perform_change_whitebalance_and_cleanup.call(values[:new])
1279 save_undo(_("fix white balance"),
1281 perform_change_whitebalance_and_cleanup.call(values[:old])
1283 autoscroll_if_needed($autotable_sw, img, textview)
1284 $notebook.set_page(1)
1286 perform_change_whitebalance_and_cleanup.call(values[:new])
1288 autoscroll_if_needed($autotable_sw, img, textview)
1289 $notebook.set_page(1)
1294 whitebalance_and_cleanup = proc {
1295 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1296 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1297 whitebalance_and_cleanup_real.call(values)
1301 gammacorrect_and_cleanup_real = proc { |values|
1302 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1303 cleanup_all_thumbnails.call
1304 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1305 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1306 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1308 perform_change_gammacorrect_and_cleanup.call(values[:new])
1310 save_undo(_("gamma correction"),
1312 perform_change_gammacorrect_and_cleanup.call(values[:old])
1314 autoscroll_if_needed($autotable_sw, img, textview)
1315 $notebook.set_page(1)
1317 perform_change_gammacorrect_and_cleanup.call(values[:new])
1319 autoscroll_if_needed($autotable_sw, img, textview)
1320 $notebook.set_page(1)
1325 gammacorrect_and_cleanup = Proc.new {
1326 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1327 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1328 gammacorrect_and_cleanup_real.call(values)
1332 enhance_and_cleanup = proc {
1333 perform_enhance_and_cleanup = proc {
1334 cleanup_all_thumbnails.call
1335 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1336 my_gen_real_thumbnail.call
1339 cleanup_all_thumbnails.call
1340 perform_enhance_and_cleanup.call
1342 save_undo(_("enhance"),
1344 perform_enhance_and_cleanup.call
1346 autoscroll_if_needed($autotable_sw, img, textview)
1347 $notebook.set_page(1)
1349 perform_enhance_and_cleanup.call
1351 autoscroll_if_needed($autotable_sw, img, textview)
1352 $notebook.set_page(1)
1357 delete = proc { |isacut|
1358 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 })
1361 perform_delete = proc {
1362 after = autotable.get_next_widget(vbox)
1364 after = autotable.get_previous_widget(vbox)
1366 if $config['deleteondisk'] && !isacut
1367 msg 3, "scheduling for delete: #{fullpath}"
1368 $todelete << fullpath
1370 autotable.remove_widget(vbox)
1372 $vbox2widgets[after][:textview].grab_focus
1373 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1377 previous_pos = autotable.get_current_number(vbox)
1381 delete_current_subalbum
1383 save_undo(_("delete"),
1385 autotable.reinsert(pos, vbox, filename)
1386 $notebook.set_page(1)
1387 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1389 msg 3, "removing deletion schedule of: #{fullpath}"
1390 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1393 $notebook.set_page(1)
1402 $cuts << { :vbox => vbox, :filename => filename }
1403 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1408 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1411 autotable.queue_draws << proc {
1412 $vbox2widgets[last[:vbox]][:textview].grab_focus
1413 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1415 save_undo(_("paste"),
1417 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1418 $notebook.set_page(1)
1421 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1423 $notebook.set_page(1)
1426 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1431 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1432 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1433 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1434 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1436 textview.signal_connect('key-press-event') { |w, event|
1439 x, y = autotable.get_current_pos(vbox)
1440 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1441 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1442 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1443 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1445 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1446 $vbox2widgets[widget_up][:textview].grab_focus
1453 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1455 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1456 $vbox2widgets[widget_down][:textview].grab_focus
1463 if event.keyval == Gdk::Keyval::GDK_Left
1466 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1473 rotate_and_cleanup.call(-90)
1476 if event.keyval == Gdk::Keyval::GDK_Right
1477 next_ = autotable.get_next_widget(vbox)
1478 if next_ && autotable.get_current_pos(next_)[0] > x
1480 $vbox2widgets[next_][:textview].grab_focus
1487 rotate_and_cleanup.call(90)
1490 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1493 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1494 view_element(filename, { :delete => delete })
1497 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1500 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1504 !propagate #- propagate if needed
1507 $ignore_next_release = false
1508 evtbox.signal_connect('button-press-event') { |w, event|
1509 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1510 if event.state & Gdk::Window::BUTTON3_MASK != 0
1511 #- gesture redo: hold right mouse button then click left mouse button
1512 $config['nogestures'] or perform_redo
1513 $ignore_next_release = true
1515 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1517 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1519 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1520 elsif $enhance.active?
1521 enhance_and_cleanup.call
1522 elsif $delete.active?
1526 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1529 $button1_pressed_autotable = true
1530 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1531 if event.state & Gdk::Window::BUTTON1_MASK != 0
1532 #- gesture undo: hold left mouse button then click right mouse button
1533 $config['nogestures'] or perform_undo
1534 $ignore_next_release = true
1536 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1537 view_element(filename, { :delete => delete })
1542 evtbox.signal_connect('button-release-event') { |w, event|
1543 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1544 if !$ignore_next_release
1545 x, y = autotable.get_current_pos(vbox)
1546 next_ = autotable.get_next_widget(vbox)
1547 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1548 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1549 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1550 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1551 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1552 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1553 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1555 $ignore_next_release = false
1556 $gesture_press = nil
1561 #- handle reordering with drag and drop
1562 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1563 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1564 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1565 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1568 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1570 #- mouse gesture first (dnd disables button-release-event)
1571 if $gesture_press && $gesture_press[:filename] == filename
1572 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1573 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1574 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1575 rotate_and_cleanup.call(angle)
1576 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1578 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1579 msg 3, "gesture delete: click-drag right button to the bottom"
1581 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1586 ctxt.targets.each { |target|
1587 if target.name == 'reorder-elements'
1588 move_dnd = proc { |from,to|
1591 autotable.move(from, to)
1592 save_undo(_("reorder"),
1595 autotable.move(to - 1, from)
1597 autotable.move(to, from + 1)
1599 $notebook.set_page(1)
1601 autotable.move(from, to)
1602 $notebook.set_page(1)
1607 if $multiple_dnd.size == 0
1608 move_dnd.call(selection_data.data.to_i,
1609 autotable.get_current_number(vbox))
1611 UndoHandler.begin_batch
1612 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1614 #- need to update current position between each call
1615 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1616 autotable.get_current_number(vbox))
1618 UndoHandler.end_batch
1629 def create_auto_table
1631 $autotable = Gtk::AutoTable.new(5)
1633 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1634 thumbnails_vb = Gtk::VBox.new(false, 5)
1636 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1637 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1638 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1639 thumbnails_vb.add($autotable)
1641 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1642 $autotable_sw.add_with_viewport(thumbnails_vb)
1644 #- follows stuff for handling multiple elements selection
1645 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1647 update_selected = proc {
1648 $autotable.current_order.each { |path|
1649 w = $name2widgets[path][:evtbox].window
1650 xm = w.position[0] + w.size[0]/2
1651 ym = w.position[1] + w.size[1]/2
1652 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1653 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1654 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1655 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1658 if $selected_elements[path] && ! $selected_elements[path][:keep]
1659 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))
1660 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1661 $selected_elements.delete(path)
1666 $autotable.signal_connect('realize') { |w,e|
1667 gc = Gdk::GC.new($autotable.window)
1668 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1669 gc.function = Gdk::GC::INVERT
1670 #- autoscroll handling for DND and multiple selections
1671 Gtk.timeout_add(100) {
1672 if ! $autotable.window.nil?
1673 w, x, y, mask = $autotable.window.pointer
1674 if mask & Gdk::Window::BUTTON1_MASK != 0
1675 if y < $autotable_sw.vadjustment.value
1677 $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]])
1679 if $button1_pressed_autotable || press_x
1680 scroll_upper($autotable_sw, y)
1683 w, pos_x, pos_y = $autotable.window.pointer
1684 $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]])
1685 update_selected.call
1688 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1690 $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]])
1692 if $button1_pressed_autotable || press_x
1693 scroll_lower($autotable_sw, y)
1696 w, pos_x, pos_y = $autotable.window.pointer
1697 $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]])
1698 update_selected.call
1703 ! $autotable.window.nil?
1707 $autotable.signal_connect('button-press-event') { |w,e|
1709 if !$button1_pressed_autotable
1712 if e.state & Gdk::Window::SHIFT_MASK == 0
1713 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1714 $selected_elements = {}
1715 $statusbar.push(0, utf8(_("Nothing selected.")))
1717 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1719 set_mousecursor(Gdk::Cursor::TCROSS)
1723 $autotable.signal_connect('button-release-event') { |w,e|
1725 if $button1_pressed_autotable
1726 #- unselect all only now
1727 $multiple_dnd = $selected_elements.keys
1728 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1729 $selected_elements = {}
1730 $button1_pressed_autotable = false
1733 $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]])
1734 if $selected_elements.length > 0
1735 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1738 press_x = press_y = pos_x = pos_y = nil
1739 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1743 $autotable.signal_connect('motion-notify-event') { |w,e|
1746 $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]])
1750 $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]])
1751 update_selected.call
1757 def create_subalbums_page
1759 subalbums_hb = Gtk::HBox.new
1760 $subalbums_vb = Gtk::VBox.new(false, 5)
1761 subalbums_hb.pack_start($subalbums_vb, false, false)
1762 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1763 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1764 $subalbums_sw.add_with_viewport(subalbums_hb)
1767 def save_current_file
1773 ios = File.open($filename, "w")
1774 $xmldoc.write(ios, 0)
1776 rescue Iconv::IllegalSequence
1777 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1778 if ! ios.nil? && ! ios.closed?
1781 $xmldoc.xml_decl.encoding = 'UTF-8'
1782 ios = File.open($filename, "w")
1783 $xmldoc.write(ios, 0)
1794 def save_current_file_user
1795 save_tempfilename = $filename
1796 $filename = $orig_filename
1797 if ! save_current_file
1798 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1799 $filename = save_tempfilename
1803 $generated_outofline = false
1804 $filename = save_tempfilename
1806 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1807 $todelete.each { |f|
1812 def mark_document_as_dirty
1813 $xmldoc.elements.each('//dir') { |elem|
1814 elem.delete_attribute('already-generated')
1818 #- ret: true => ok false => cancel
1819 def ask_save_modifications(msg1, msg2, *options)
1821 options = options.size > 0 ? options[0] : {}
1823 if options[:disallow_cancel]
1824 dialog = Gtk::Dialog.new(msg1,
1826 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1827 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1828 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1830 dialog = Gtk::Dialog.new(msg1,
1832 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1833 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1834 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1835 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1837 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1838 dialog.vbox.add(Gtk::Label.new(msg2))
1839 dialog.window_position = Gtk::Window::POS_CENTER
1842 dialog.run { |response|
1844 if response == Gtk::Dialog::RESPONSE_YES
1845 if ! save_current_file_user
1846 return ask_save_modifications(msg1, msg2, options)
1849 #- if we have generated an album but won't save modifications, we must remove
1850 #- already-generated markers in original file
1851 if $generated_outofline
1853 $xmldoc = REXML::Document.new File.new($orig_filename)
1854 mark_document_as_dirty
1855 ios = File.open($orig_filename, "w")
1856 $xmldoc.write(ios, 0)
1859 puts "exception: #{$!}"
1863 if response == Gtk::Dialog::RESPONSE_CANCEL
1866 $todelete = [] #- unconditionally clear the list of images/videos to delete
1872 def try_quit(*options)
1873 if ask_save_modifications(utf8(_("Save before quitting?")),
1874 utf8(_("Do you want to save your changes before quitting?")),
1880 def show_popup(parent, msg, *options)
1881 dialog = Gtk::Dialog.new
1882 if options[0] && options[0][:title]
1883 dialog.title = options[0][:title]
1885 dialog.title = utf8(_("Booh message"))
1887 lbl = Gtk::Label.new
1888 if options[0] && options[0][:nomarkup]
1893 if options[0] && options[0][:centered]
1894 lbl.set_justify(Gtk::Justification::CENTER)
1896 if options[0] && options[0][:selectable]
1897 lbl.selectable = true
1899 if options[0] && options[0][:topwidget]
1900 dialog.vbox.add(options[0][:topwidget])
1902 if options[0] && options[0][:scrolled]
1903 sw = Gtk::ScrolledWindow.new(nil, nil)
1904 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1905 sw.add_with_viewport(lbl)
1907 dialog.set_default_size(500, 600)
1909 dialog.vbox.add(lbl)
1910 dialog.set_default_size(200, 120)
1912 if options[0] && options[0][:okcancel]
1913 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1915 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1917 if options[0] && options[0][:pos_centered]
1918 dialog.window_position = Gtk::Window::POS_CENTER
1920 dialog.window_position = Gtk::Window::POS_MOUSE
1923 if options[0] && options[0][:linkurl]
1924 linkbut = Gtk::Button.new('')
1925 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1926 linkbut.signal_connect('clicked') {
1927 open_url(options[0][:linkurl])
1928 dialog.response(Gtk::Dialog::RESPONSE_OK)
1929 set_mousecursor_normal
1931 linkbut.relief = Gtk::RELIEF_NONE
1932 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1933 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1934 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1939 if !options[0] || !options[0][:not_transient]
1940 dialog.transient_for = parent
1941 dialog.run { |response|
1943 if options[0] && options[0][:okcancel]
1944 return response == Gtk::Dialog::RESPONSE_OK
1948 dialog.signal_connect('response') { dialog.destroy }
1952 def backend_wait_message(parent, msg, infopipe_path, mode)
1954 w.set_transient_for(parent)
1957 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1958 vb.pack_start(Gtk::Label.new(msg), false, false)
1960 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1961 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1962 if mode != 'one dir scan'
1963 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1965 if mode == 'web-album'
1966 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1967 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1969 vb.pack_start(Gtk::HSeparator.new, false, false)
1971 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1972 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1973 vb.pack_end(bottom, false, false)
1975 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1976 refresh_thread = Thread.new {
1977 directories_counter = 0
1978 while line = infopipe.gets
1979 if line =~ /^directories: (\d+), sizes: (\d+)/
1980 directories = $1.to_f + 1
1982 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1983 elements = $3.to_f + 1
1984 if mode == 'web-album'
1988 gtk_thread_protect { pb1_1.fraction = 0 }
1989 if mode != 'one dir scan'
1990 newtext = utf8(full_src_dir_to_rel($1, $2))
1991 newtext = '/' if newtext == ''
1992 gtk_thread_protect { pb1_2.text = newtext }
1993 directories_counter += 1
1994 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1996 elsif line =~ /^processing element$/
1997 element_counter += 1
1998 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1999 elsif line =~ /^processing size$/
2000 element_counter += 1
2001 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
2002 elsif line =~ /^finished processing sizes$/
2003 gtk_thread_protect { pb1_1.fraction = 1 }
2004 elsif line =~ /^creating index.html$/
2005 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2006 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2007 directories_counter = 0
2008 elsif line =~ /^index.html: (.+)\|(.+)/
2009 newtext = utf8(full_src_dir_to_rel($1, $2))
2010 newtext = '/' if newtext == ''
2011 gtk_thread_protect { pb2.text = newtext }
2012 directories_counter += 1
2013 gtk_thread_protect { pb2.fraction = directories_counter / directories }
2014 elsif line =~ /^die: (.*)$/
2021 w.signal_connect('delete-event') { w.destroy }
2022 w.signal_connect('destroy') {
2023 Thread.kill(refresh_thread)
2024 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2027 File.delete(infopipe_path)
2030 w.window_position = Gtk::Window::POS_CENTER
2036 def call_backend(cmd, waitmsg, mode, params)
2037 pipe = Tempfile.new("boohpipe")
2039 system("mkfifo #{pipe.path}")
2040 cmd += " --info-pipe #{pipe.path}"
2041 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2046 id, exitstatus = Process.waitpid2(pid)
2047 gtk_thread_protect { w8.destroy }
2049 if params[:successmsg]
2050 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2052 if params[:closure_after]
2053 gtk_thread_protect(¶ms[:closure_after])
2055 elsif exitstatus == 15
2056 #- say nothing, user aborted
2058 gtk_thread_protect { show_popup($main_window,
2059 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2065 button.signal_connect('clicked') {
2066 Process.kill('SIGTERM', pid)
2070 def save_changes(*forced)
2071 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2075 $xmldir.delete_attribute('already-generated')
2077 propagate_children = proc { |xmldir|
2078 if xmldir.attributes['subdirs-caption']
2079 xmldir.delete_attribute('already-generated')
2081 xmldir.elements.each('dir') { |element|
2082 propagate_children.call(element)
2086 if $xmldir.child_byname_notattr('dir', 'deleted')
2087 new_title = $subalbums_title.buffer.text
2088 if new_title != $xmldir.attributes['subdirs-caption']
2089 parent = $xmldir.parent
2090 if parent.name == 'dir'
2091 parent.delete_attribute('already-generated')
2093 propagate_children.call($xmldir)
2095 $xmldir.add_attribute('subdirs-caption', new_title)
2096 $xmldir.elements.each('dir') { |element|
2097 if !element.attributes['deleted']
2098 path = element.attributes['path']
2099 newtext = $subalbums_edits[path][:editzone].buffer.text
2100 if element.attributes['subdirs-caption']
2101 if element.attributes['subdirs-caption'] != newtext
2102 propagate_children.call(element)
2104 element.add_attribute('subdirs-caption', newtext)
2105 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2107 if element.attributes['thumbnails-caption'] != newtext
2108 element.delete_attribute('already-generated')
2110 element.add_attribute('thumbnails-caption', newtext)
2111 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2117 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2118 if $xmldir.attributes['thumbnails-caption']
2119 path = $xmldir.attributes['path']
2120 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2122 elsif $xmldir.attributes['thumbnails-caption']
2123 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2126 if $xmldir.attributes['thumbnails-caption']
2127 if edit = $subalbums_edits[$xmldir.attributes['path']]
2128 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2132 #- remove and reinsert elements to reflect new ordering
2135 $xmldir.elements.each { |element|
2136 if element.name == 'image' || element.name == 'video'
2137 saves[element.attributes['filename']] = element.remove
2141 $autotable.current_order.each { |path|
2142 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2143 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2146 saves.each_key { |path|
2147 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2148 chld.add_attribute('deleted', 'true')
2152 def sort_by_exif_date
2156 $xmldir.elements.each { |element|
2157 if element.name == 'image' || element.name == 'video'
2158 current_order << element.attributes['filename']
2162 #- look for EXIF dates
2165 if current_order.size > 20
2167 w.set_transient_for($main_window)
2169 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2170 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2171 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2172 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2173 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2174 vb.pack_end(bottom, false, false)
2176 w.signal_connect('delete-event') { w.destroy }
2177 w.window_position = Gtk::Window::POS_CENTER
2181 b.signal_connect('clicked') { aborted = true }
2183 current_order.each { |f|
2185 if entry2type(f) == 'image'
2187 pb.fraction = i.to_f / current_order.size
2188 Gtk.main_iteration while Gtk.events_pending?
2189 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2191 dates[f] = date_time
2204 current_order.each { |f|
2205 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2207 dates[f] = date_time
2213 $xmldir.elements.each { |element|
2214 if element.name == 'image' || element.name == 'video'
2215 saves[element.attributes['filename']] = element.remove
2219 neworder = smartsort(current_order, dates)
2222 $xmldir.add_element(saves[f].name, saves[f].attributes)
2225 #- let the auto-table reflect new ordering
2229 def remove_all_captions
2232 $autotable.current_order.each { |path|
2233 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2234 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2236 save_undo(_("remove all captions"),
2238 texts.each_key { |key|
2239 $name2widgets[key][:textview].buffer.text = texts[key]
2241 $notebook.set_page(1)
2243 texts.each_key { |key|
2244 $name2widgets[key][:textview].buffer.text = ''
2246 $notebook.set_page(1)
2252 $selected_elements.each_key { |path|
2253 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2259 $selected_elements = {}
2263 $undo_tb.sensitive = $undo_mb.sensitive = false
2264 $redo_tb.sensitive = $redo_mb.sensitive = false
2270 $subalbums_vb.children.each { |chld|
2271 $subalbums_vb.remove(chld)
2273 $subalbums = Gtk::Table.new(0, 0, true)
2274 current_y_sub_albums = 0
2276 $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"])
2277 $subalbums_edits = {}
2278 subalbums_counter = 0
2279 subalbums_edits_bypos = {}
2281 add_subalbum = proc { |xmldir, counter|
2282 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2283 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2284 if xmldir == $xmldir
2285 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2286 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2287 caption = xmldir.attributes['thumbnails-caption']
2288 infotype = 'thumbnails'
2290 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2291 captionfile, caption = find_subalbum_caption_info(xmldir)
2292 infotype = find_subalbum_info_type(xmldir)
2294 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2295 hbox = Gtk::HBox.new
2296 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2298 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2301 my_gen_real_thumbnail = proc {
2302 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2305 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2306 f.add(img = Gtk::Image.new)
2307 my_gen_real_thumbnail.call
2309 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2311 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2312 $subalbums.attach(hbox,
2313 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2315 frame, textview = create_editzone($subalbums_sw, 0, img)
2316 textview.buffer.text = caption
2317 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2318 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2320 change_image = proc {
2321 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2323 Gtk::FileChooser::ACTION_OPEN,
2325 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2326 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2327 fc.transient_for = $main_window
2328 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))
2329 f.add(preview_img = Gtk::Image.new)
2331 fc.signal_connect('update-preview') { |w|
2333 if fc.preview_filename
2334 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2335 fc.preview_widget_active = true
2337 rescue Gdk::PixbufError
2338 fc.preview_widget_active = false
2341 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2343 old_file = captionfile
2344 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2345 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2346 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2347 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2349 new_file = fc.filename
2350 msg 3, "new captionfile is: #{fc.filename}"
2351 perform_changefile = proc {
2352 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2353 $modified_pixbufs.delete(thumbnail_file)
2354 xmldir.delete_attribute("#{infotype}-rotate")
2355 xmldir.delete_attribute("#{infotype}-color-swap")
2356 xmldir.delete_attribute("#{infotype}-enhance")
2357 xmldir.delete_attribute("#{infotype}-seektime")
2358 my_gen_real_thumbnail.call
2360 perform_changefile.call
2362 save_undo(_("change caption file for sub-album"),
2364 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2365 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2366 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2367 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2368 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2369 my_gen_real_thumbnail.call
2370 $notebook.set_page(0)
2372 perform_changefile.call
2373 $notebook.set_page(0)
2381 if File.exists?(thumbnail_file)
2382 File.delete(thumbnail_file)
2384 my_gen_real_thumbnail.call
2387 rotate_and_cleanup = proc { |angle|
2388 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2389 if File.exists?(thumbnail_file)
2390 File.delete(thumbnail_file)
2394 move = proc { |direction|
2397 save_changes('forced')
2398 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2399 if direction == 'up'
2400 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2401 subalbums_edits_bypos[oldpos - 1][:position] += 1
2403 if direction == 'down'
2404 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2405 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2407 if direction == 'top'
2408 for i in 1 .. oldpos - 1
2409 subalbums_edits_bypos[i][:position] += 1
2411 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2413 if direction == 'bottom'
2414 for i in oldpos + 1 .. subalbums_counter
2415 subalbums_edits_bypos[i][:position] -= 1
2417 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2421 $xmldir.elements.each('dir') { |element|
2422 if (!element.attributes['deleted'])
2423 elems << [ element.attributes['path'], element.remove ]
2426 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2427 each { |e| $xmldir.add_element(e[1]) }
2428 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2429 $xmldir.elements.each('descendant::dir') { |elem|
2430 elem.delete_attribute('already-generated')
2433 sel = $albums_tv.selection.selected_rows
2435 populate_subalbums_treeview(false)
2436 $albums_tv.selection.select_path(sel[0])
2439 color_swap_and_cleanup = proc {
2440 perform_color_swap_and_cleanup = proc {
2441 color_swap(xmldir, "#{infotype}-")
2442 my_gen_real_thumbnail.call
2444 perform_color_swap_and_cleanup.call
2446 save_undo(_("color swap"),
2448 perform_color_swap_and_cleanup.call
2449 $notebook.set_page(0)
2451 perform_color_swap_and_cleanup.call
2452 $notebook.set_page(0)
2457 change_seektime_and_cleanup = proc {
2458 if values = ask_new_seektime(xmldir, "#{infotype}-")
2459 perform_change_seektime_and_cleanup = proc { |val|
2460 change_seektime(xmldir, "#{infotype}-", val)
2461 my_gen_real_thumbnail.call
2463 perform_change_seektime_and_cleanup.call(values[:new])
2465 save_undo(_("specify seektime"),
2467 perform_change_seektime_and_cleanup.call(values[:old])
2468 $notebook.set_page(0)
2470 perform_change_seektime_and_cleanup.call(values[:new])
2471 $notebook.set_page(0)
2477 whitebalance_and_cleanup = proc {
2478 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2479 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2480 perform_change_whitebalance_and_cleanup = proc { |val|
2481 change_whitebalance(xmldir, "#{infotype}-", val)
2482 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2483 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2484 if File.exists?(thumbnail_file)
2485 File.delete(thumbnail_file)
2488 perform_change_whitebalance_and_cleanup.call(values[:new])
2490 save_undo(_("fix white balance"),
2492 perform_change_whitebalance_and_cleanup.call(values[:old])
2493 $notebook.set_page(0)
2495 perform_change_whitebalance_and_cleanup.call(values[:new])
2496 $notebook.set_page(0)
2502 gammacorrect_and_cleanup = proc {
2503 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2504 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2505 perform_change_gammacorrect_and_cleanup = proc { |val|
2506 change_gammacorrect(xmldir, "#{infotype}-", val)
2507 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2508 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2509 if File.exists?(thumbnail_file)
2510 File.delete(thumbnail_file)
2513 perform_change_gammacorrect_and_cleanup.call(values[:new])
2515 save_undo(_("gamma correction"),
2517 perform_change_gammacorrect_and_cleanup.call(values[:old])
2518 $notebook.set_page(0)
2520 perform_change_gammacorrect_and_cleanup.call(values[:new])
2521 $notebook.set_page(0)
2527 enhance_and_cleanup = proc {
2528 perform_enhance_and_cleanup = proc {
2529 enhance(xmldir, "#{infotype}-")
2530 my_gen_real_thumbnail.call
2533 perform_enhance_and_cleanup.call
2535 save_undo(_("enhance"),
2537 perform_enhance_and_cleanup.call
2538 $notebook.set_page(0)
2540 perform_enhance_and_cleanup.call
2541 $notebook.set_page(0)
2546 evtbox.signal_connect('button-press-event') { |w, event|
2547 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2549 rotate_and_cleanup.call(90)
2551 rotate_and_cleanup.call(-90)
2552 elsif $enhance.active?
2553 enhance_and_cleanup.call
2556 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2557 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2558 { :forbid_left => true, :forbid_right => true,
2559 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2560 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2561 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2562 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2563 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2565 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2570 evtbox.signal_connect('button-press-event') { |w, event|
2571 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2575 evtbox.signal_connect('button-release-event') { |w, event|
2576 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2577 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2578 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2579 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2580 msg 3, "gesture rotate: #{angle}"
2581 rotate_and_cleanup.call(angle)
2584 $gesture_press = nil
2587 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2588 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2589 current_y_sub_albums += 1
2592 if $xmldir.child_byname_notattr('dir', 'deleted')
2594 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2595 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2596 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2597 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2598 #- this album image/caption
2599 if $xmldir.attributes['thumbnails-caption']
2600 add_subalbum.call($xmldir, 0)
2603 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2604 $xmldir.elements.each { |element|
2605 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2606 #- element (image or video) of this album
2607 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2608 msg 3, "dest_img: #{dest_img}"
2609 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2610 total[element.name] += 1
2612 if element.name == 'dir' && !element.attributes['deleted']
2613 #- sub-album image/caption
2614 add_subalbum.call(element, subalbums_counter += 1)
2615 total[element.name] += 1
2618 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2619 total['image'], total['video'], total['dir'] ]))
2620 $subalbums_vb.add($subalbums)
2621 $subalbums_vb.show_all
2623 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2624 $notebook.get_tab_label($autotable_sw).sensitive = false
2625 $notebook.set_page(0)
2626 $thumbnails_title.buffer.text = ''
2628 $notebook.get_tab_label($autotable_sw).sensitive = true
2629 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2632 if !$xmldir.child_byname_notattr('dir', 'deleted')
2633 $notebook.get_tab_label($subalbums_sw).sensitive = false
2634 $notebook.set_page(1)
2636 $notebook.get_tab_label($subalbums_sw).sensitive = true
2640 def pixbuf_or_nil(filename)
2642 return Gdk::Pixbuf.new(filename)
2648 def theme_choose(current)
2649 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2651 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2652 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2653 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2655 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2656 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2657 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2658 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2659 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2660 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2661 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2662 treeview.signal_connect('button-press-event') { |w, event|
2663 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2664 dialog.response(Gtk::Dialog::RESPONSE_OK)
2668 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2670 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2673 iter[0] = File.basename(dir)
2674 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2675 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2676 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2677 if File.basename(dir) == current
2678 treeview.selection.select_iter(iter)
2682 dialog.set_default_size(700, 400)
2683 dialog.vbox.show_all
2684 dialog.run { |response|
2685 iter = treeview.selection.selected
2687 if response == Gtk::Dialog::RESPONSE_OK && iter
2688 return model.get_value(iter, 0)
2694 def show_password_protections
2695 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2696 child_iter = $albums_iters[xmldir.attributes['path']]
2697 if xmldir.attributes['password-protect']
2698 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2699 already_protected = true
2700 elsif already_protected
2701 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2703 pix = pix.saturate_and_pixelate(1, true)
2709 xmldir.elements.each('dir') { |elem|
2710 if !elem.attributes['deleted']
2711 examine_dir_elem.call(child_iter, elem, already_protected)
2715 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2718 def populate_subalbums_treeview(select_first)
2722 $subalbums_vb.children.each { |chld|
2723 $subalbums_vb.remove(chld)
2726 source = $xmldoc.root.attributes['source']
2727 msg 3, "source: #{source}"
2729 xmldir = $xmldoc.elements['//dir']
2730 if !xmldir || xmldir.attributes['path'] != source
2731 msg 1, _("Corrupted booh file...")
2735 append_dir_elem = proc { |parent_iter, xmldir|
2736 child_iter = $albums_ts.append(parent_iter)
2737 child_iter[0] = File.basename(xmldir.attributes['path'])
2738 child_iter[1] = xmldir.attributes['path']
2739 $albums_iters[xmldir.attributes['path']] = child_iter
2740 msg 3, "puttin location: #{xmldir.attributes['path']}"
2741 xmldir.elements.each('dir') { |elem|
2742 if !elem.attributes['deleted']
2743 append_dir_elem.call(child_iter, elem)
2747 append_dir_elem.call(nil, xmldir)
2748 show_password_protections
2750 $albums_tv.expand_all
2752 $albums_tv.selection.select_iter($albums_ts.iter_first)
2756 def select_current_theme
2757 select_theme($xmldoc.root.attributes['theme'],
2758 $xmldoc.root.attributes['limit-sizes'],
2759 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2760 $xmldoc.root.attributes['thumbnails-per-row'])
2763 def open_file(filename)
2767 $current_path = nil #- invalidate
2768 $modified_pixbufs = {}
2771 $subalbums_vb.children.each { |chld|
2772 $subalbums_vb.remove(chld)
2775 if !File.exists?(filename)
2776 return utf8(_("File not found."))
2780 $xmldoc = REXML::Document.new File.new(filename)
2785 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2786 if entry2type(filename).nil?
2787 return utf8(_("Not a booh file!"))
2789 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."))
2793 if !source = $xmldoc.root.attributes['source']
2794 return utf8(_("Corrupted booh file..."))
2797 if !dest = $xmldoc.root.attributes['destination']
2798 return utf8(_("Corrupted booh file..."))
2801 if !theme = $xmldoc.root.attributes['theme']
2802 return utf8(_("Corrupted booh file..."))
2805 if $xmldoc.root.attributes['version'] < '0.8.99.2'
2806 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2807 mark_document_as_dirty
2808 if $xmldoc.root.attributes['version'] < '0.8.4'
2809 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2810 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2811 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2812 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2813 if old_dest_dir != new_dest_dir
2814 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2816 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2817 xmldir.elements.each { |element|
2818 if %w(image video).include?(element.name) && !element.attributes['deleted']
2819 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2820 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2821 Dir[old_name + '*'].each { |file|
2822 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2823 file != new_file and sys("mv '#{file}' '#{new_file}'")
2826 if element.name == 'dir' && !element.attributes['deleted']
2827 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2828 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2829 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2833 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2837 $xmldoc.root.add_attribute('version', $VERSION)
2840 select_current_theme
2842 $filename = filename
2843 $default_size['thumbnails'] =~ /(.*)x(.*)/
2844 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2845 $albums_thumbnail_size =~ /(.*)x(.*)/
2846 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2848 populate_subalbums_treeview(true)
2850 $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
2854 def open_file_user(filename)
2855 result = open_file(filename)
2857 $config['last-opens'] ||= []
2858 if $config['last-opens'][-1] != utf8(filename)
2859 $config['last-opens'] << utf8(filename)
2861 $orig_filename = $filename
2862 tmp = Tempfile.new("boohtemp")
2865 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2867 $tempfiles << $filename << "#{$filename}.backup"
2869 $orig_filename = nil
2875 if !ask_save_modifications(utf8(_("Save this album?")),
2876 utf8(_("Do you want to save the changes to this album?")),
2877 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2880 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2882 Gtk::FileChooser::ACTION_OPEN,
2884 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2885 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2886 fc.set_current_folder(File.expand_path("~/.booh"))
2887 fc.transient_for = $main_window
2890 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2891 push_mousecursor_wait(fc)
2892 msg = open_file_user(fc.filename)
2907 def additional_booh_options
2910 options += "--mproc #{$config['mproc'].to_i} "
2912 options += "--comments-format '#{$config['comments-format']}'"
2916 def ask_multi_languages(value)
2918 spl = value.split(',')
2919 value = [ spl[0..-2], spl[-1] ]
2922 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
2925 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2926 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2928 lbl = Gtk::Label.new
2930 _("You can choose to activate <b>multi-languages</b> support for this web-album
2931 (it will work only if you publish your web-album on an Apache web-server). This will
2932 use the MultiViews feature of Apache; the pages will be served according to the
2933 value of the Accept-Language HTTP header sent by the web browsers, so that people
2934 with different languages preferences will be able to browse your web-album with
2935 navigation in their language (if language is available).
2938 dialog.vbox.add(lbl)
2939 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
2940 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
2941 add(languages = Gtk::Button.new))))
2943 pick_languages = proc {
2944 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
2947 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2948 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2950 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
2951 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
2953 SUPPORTED_LANGUAGES.each { |lang|
2954 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
2955 if ! value.nil? && value[0].include?(lang)
2961 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
2962 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
2963 fallback_language = nil
2964 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
2965 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
2966 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
2967 fbl_rb.active = true
2968 fallback_language = SUPPORTED_LANGUAGES[0]
2970 SUPPORTED_LANGUAGES[1..-1].each { |lang|
2971 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
2972 rb.signal_connect('clicked') { fallback_language = lang }
2973 if ! value.nil? && value[1] == lang
2978 dialog2.window_position = Gtk::Window::POS_MOUSE
2982 dialog2.run { |response|
2984 if resp == Gtk::Dialog::RESPONSE_OK
2986 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
2987 value[1] = fallback_language
2988 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| utf8(langname(v)) }.join(', '), utf8(langname(value[1])) ])
2995 languages.signal_connect('clicked') {
2998 dialog.window_position = Gtk::Window::POS_MOUSE
3002 rb_yes.active = true
3003 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| utf8(langname(v)) }.join(', '), utf8(langname(value[1])) ])
3005 rb_no.signal_connect('clicked') {
3009 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3022 dialog.run { |response|
3027 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3029 return [ true, nil ]
3031 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3040 if !ask_save_modifications(utf8(_("Save this album?")),
3041 utf8(_("Do you want to save the changes to this album?")),
3042 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3045 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3047 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3048 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3049 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3051 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3052 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
3053 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3054 tbl.attach(src = Gtk::Entry.new,
3055 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3056 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3057 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3058 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
3059 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3060 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3061 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3062 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3063 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3064 tbl.attach(dest = Gtk::Entry.new,
3065 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3066 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3067 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3068 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3069 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3070 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3071 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3072 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3073 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3075 tooltips = Gtk::Tooltips.new
3076 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3077 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3078 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3079 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3080 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3081 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
3082 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)
3083 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3084 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3085 nperpage_model = Gtk::ListStore.new(String, String)
3086 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3087 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3088 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3089 nperpagecombo.set_attributes(crt, { :markup => 0 })
3090 iter = nperpage_model.append
3091 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3093 [ 12, 20, 30, 40, 50 ].each { |v|
3094 iter = nperpage_model.append
3095 iter[0] = iter[1] = v.to_s
3097 nperpagecombo.active = 0
3099 multilanguages_value = nil
3100 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new(utf8(_("Multi-languages: disabled."))), false, false, 0).
3101 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3102 tooltips.set_tip(ml, utf8(_("When disabled, the web-album will be generated with navigation in your desktop language. When enabled, the web-album will be generated with navigation in all languages you select, but you have to publish your web-album on an Apache web-server for that feature to work.")), nil)
3103 multilanguages.signal_connect('clicked') {
3104 retval = ask_multi_languages(multilanguages_value)
3106 multilanguages_value = retval[1]
3108 if multilanguages_value
3109 ml_label.text = utf8(_("Multi-languages: enabled."))
3111 ml_label.text = utf8(_("Multi-languages: disabled."))
3115 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3116 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3117 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)
3118 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3119 pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
3120 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)
3122 src_nb_calculated_for = ''
3124 process_src_nb = proc {
3125 if src.text != src_nb_calculated_for
3126 src_nb_calculated_for = src.text
3128 Thread.kill(src_nb_thread)
3131 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3132 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3134 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3135 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3136 src_nb_thread = Thread.new {
3137 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3138 total = { 'image' => 0, 'video' => 0, nil => 0 }
3139 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3140 if File.basename(dir) =~ /^\./
3144 Dir.entries(dir.chomp).each { |file|
3145 total[entry2type(file)] += 1
3147 rescue Errno::EACCES, Errno::ENOENT
3151 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3155 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3158 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3164 timeout_src_nb = Gtk.timeout_add(100) {
3168 src_browse.signal_connect('clicked') {
3169 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
3171 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3173 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3174 fc.transient_for = $main_window
3175 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3176 src.text = utf8(fc.filename)
3178 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3183 dest_browse.signal_connect('clicked') {
3184 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3186 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3188 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3189 fc.transient_for = $main_window
3190 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3191 dest.text = utf8(fc.filename)
3196 conf_browse.signal_connect('clicked') {
3197 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3199 Gtk::FileChooser::ACTION_SAVE,
3201 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3202 fc.transient_for = $main_window
3203 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3204 fc.set_current_folder(File.expand_path("~/.booh"))
3205 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3206 conf.text = utf8(fc.filename)
3213 recreate_theme_config = proc {
3214 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3216 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3217 $images_size.each { |s|
3218 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3222 tooltips.set_tip(cb, utf8(s['description']), nil)
3223 theme_sizes << { :widget => cb, :value => s['name'] }
3225 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3226 tooltips = Gtk::Tooltips.new
3227 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3228 theme_sizes << { :widget => cb, :value => 'original' }
3231 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3234 $allowed_N_values.each { |n|
3236 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3238 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3240 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3244 nperrows << { :widget => rb, :value => n }
3246 nperrowradios.show_all
3248 recreate_theme_config.call
3250 theme_button.signal_connect('clicked') {
3251 if newtheme = theme_choose(theme_button.label)
3252 theme_button.label = newtheme
3253 recreate_theme_config.call
3257 dialog.vbox.add(frame1)
3258 dialog.vbox.add(frame2)
3264 dialog.run { |response|
3265 if response == Gtk::Dialog::RESPONSE_OK
3266 srcdir = from_utf8_safe(src.text)
3267 destdir = from_utf8_safe(dest.text)
3268 confpath = from_utf8_safe(conf.text)
3269 if src.text != '' && srcdir == ''
3270 show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3272 elsif !File.directory?(srcdir)
3273 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3275 elsif dest.text != '' && destdir == ''
3276 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3278 elsif destdir != make_dest_filename(destdir)
3279 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3281 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3282 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
3284 elsif File.exists?(destdir) && !File.directory?(destdir)
3285 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3287 elsif conf.text == ''
3288 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3290 elsif conf.text != '' && confpath == ''
3291 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3293 elsif File.directory?(confpath)
3294 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3296 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3297 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3299 system("mkdir '#{destdir}'")
3300 if !File.directory?(destdir)
3301 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3313 srcdir = from_utf8(src.text)
3314 destdir = from_utf8(dest.text)
3315 configskel = File.expand_path(from_utf8(conf.text))
3316 theme = theme_button.label
3317 #- some sort of automatic theme preference
3318 $config['default-theme'] = theme
3319 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3320 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3321 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3322 opt432 = optimize432.active?
3323 madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3324 indexlink = indexlinkentry.text.gsub('\'', ''')
3327 Thread.kill(src_nb_thread)
3328 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3331 Gtk.timeout_remove(timeout_src_nb)
3334 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3335 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3336 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3337 (multilanguages_value ? "--multi-languages #{multilanguages_value}" : '') +
3338 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3339 utf8(_("Please wait while scanning source directory...")),
3341 { :closure_after => proc { open_file_user(configskel) } })
3346 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3348 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3349 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3350 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3352 source = $xmldoc.root.attributes['source']
3353 dest = $xmldoc.root.attributes['destination']
3354 theme = $xmldoc.root.attributes['theme']
3355 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3356 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3357 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3358 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3360 limit_sizes = limit_sizes.split(/,/)
3362 madewith = ($xmldoc.root.attributes['made-with'] || '').gsub(''', '\'')
3363 indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub(''', '\'')
3364 save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3366 tooltips = Gtk::Tooltips.new
3367 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3368 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3369 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3370 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3371 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3372 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3373 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3374 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3375 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3376 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3377 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3378 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3379 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3381 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3382 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3383 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3384 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3385 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3386 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3387 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)
3388 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3389 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3390 nperpage_model = Gtk::ListStore.new(String, String)
3391 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3392 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3393 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3394 nperpagecombo.set_attributes(crt, { :markup => 0 })
3395 iter = nperpage_model.append
3396 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3398 [ 12, 20, 30, 40, 50 ].each { |v|
3399 iter = nperpage_model.append
3400 iter[0] = iter[1] = v.to_s
3401 if nperpage && nperpage == v.to_s
3402 nperpagecombo.active_iter = iter
3405 if nperpagecombo.active_iter.nil?
3406 nperpagecombo.active = 0
3409 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3410 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3411 tooltips.set_tip(ml, utf8(_("When disabled, the web-album will be generated with navigation in your desktop language. When enabled, the web-album will be generated with navigation in all languages you select, but you have to publish your web-album on an Apache web-server for that feature to work.")), nil)
3413 if save_multilanguages_value
3414 ml_label.text = utf8(_("Multi-languages: enabled."))
3416 ml_label.text = utf8(_("Multi-languages: disabled."))
3420 multilanguages.signal_connect('clicked') {
3421 retval = ask_multi_languages(save_multilanguages_value)
3423 save_multilanguages_value = retval[1]
3428 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3429 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3431 indexlinkentry.text = indexlink
3433 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)
3434 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3435 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3437 madewithentry.text = madewith
3439 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)
3443 recreate_theme_config = proc {
3444 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3446 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3448 $images_size.each { |s|
3449 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3451 if limit_sizes.include?(s['name'])
3459 tooltips.set_tip(cb, utf8(s['description']), nil)
3460 theme_sizes << { :widget => cb, :value => s['name'] }
3462 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3463 tooltips = Gtk::Tooltips.new
3464 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3465 if limit_sizes && limit_sizes.include?('original')
3468 theme_sizes << { :widget => cb, :value => 'original' }
3471 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3474 $allowed_N_values.each { |n|
3476 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3478 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3480 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3481 nperrowradios.add(Gtk::Label.new(' '))
3482 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3485 nperrows << { :widget => rb, :value => n.to_s }
3487 nperrowradios.show_all
3489 recreate_theme_config.call
3491 theme_button.signal_connect('clicked') {
3492 if newtheme = theme_choose(theme_button.label)
3495 theme_button.label = newtheme
3496 recreate_theme_config.call
3500 dialog.vbox.add(frame1)
3501 dialog.vbox.add(frame2)
3507 dialog.run { |response|
3508 if response == Gtk::Dialog::RESPONSE_OK
3509 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3510 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3519 save_theme = theme_button.label
3520 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3521 save_opt432 = optimize432.active?
3522 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3523 save_nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3524 save_madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3525 save_indexlink = indexlinkentry.text.gsub('\'', ''')
3528 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 || save_multilanguages_value != multilanguages_value)
3529 if save_theme != theme
3530 #- some sort of automatic theme preference
3531 $config['default-theme'] = save_theme
3533 mark_document_as_dirty
3535 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3536 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3537 (save_nperpage ? "--thumbnails-per-page #{save_nperpage} " : '') +
3538 (save_multilanguages_value ? "--multi-languages #{save_multilanguages_value}" : '') +
3539 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3540 utf8(_("Please wait while scanning source directory...")),
3542 { :closure_after => proc {
3543 open_file($filename)
3547 #- select_theme merges global variables, need to return to current choices
3548 select_current_theme
3555 sel = $albums_tv.selection.selected_rows
3557 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3558 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3559 utf8(_("Please wait while scanning source directory...")),
3561 { :closure_after => proc {
3562 open_file($filename)
3563 $albums_tv.selection.select_path(sel[0])
3571 sel = $albums_tv.selection.selected_rows
3573 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3574 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3575 utf8(_("Please wait while scanning source directory...")),
3577 { :closure_after => proc {
3578 open_file($filename)
3579 $albums_tv.selection.select_path(sel[0])
3587 theme = $xmldoc.root.attributes['theme']
3588 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3590 limit_sizes = "--sizes #{limit_sizes}"
3592 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3593 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3594 utf8(_("Please wait while scanning source directory...")),
3596 { :closure_after => proc {
3597 open_file($filename)
3603 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3605 Gtk::FileChooser::ACTION_SAVE,
3607 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3608 fc.transient_for = $main_window
3609 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3610 fc.set_current_folder(File.expand_path("~/.booh"))
3611 fc.filename = $orig_filename
3612 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3613 $orig_filename = fc.filename
3614 if ! save_current_file_user
3618 $config['last-opens'] ||= []
3619 $config['last-opens'] << $orig_filename
3625 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3627 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3628 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3629 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3631 dialog.vbox.add(notebook = Gtk::Notebook.new)
3632 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3633 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3634 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3635 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)),
3636 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3637 tooltips = Gtk::Tooltips.new
3638 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3639 for example: /usr/bin/mplayer %f")), nil)
3640 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3641 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3642 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3643 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3644 tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3645 for example: /usr/bin/gimp-remote %f")), nil)
3646 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3647 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3648 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3649 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3650 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3651 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3652 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3653 0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3654 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)),
3655 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3656 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)
3657 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3658 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3659 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)
3660 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3661 0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3662 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)
3664 smp_check.signal_connect('toggled') {
3665 if smp_check.active?
3666 smp_hbox.sensitive = true
3668 smp_hbox.sensitive = false
3672 smp_check.active = true
3673 smp_spin.value = $config['mproc'].to_i
3675 nogestures_check.active = $config['nogestures']
3676 deleteondisk_check.active = $config['deleteondisk']
3678 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3679 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3680 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3681 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3682 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3683 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3684 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3685 tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3686 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3687 tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3688 2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3689 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)
3690 commentsformat_help.signal_connect('clicked') {
3691 show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3692 hence you should look at ImageMagick/identify documentation for the most
3693 accurate and up-to-date documentation. Last time I checked, documentation
3696 Print information about the image in a format of your choosing. You can
3697 include the image filename, type, width, height, Exif data, or other image
3698 attributes by embedding special format characters:
3701 %P page width and height
3705 %e filename extension
3710 %k number of unique colors
3717 %r image class and colorspace
3720 %u unique temporary filename
3733 displays MIFF:bird.miff 512x480 for an image titled bird.miff and whose
3734 width is 512 and height is 480.
3736 If the first character of string is @, the format is read from a file titled
3737 by the remaining characters in the string.
3739 You can also use the following special formatting syntax to print Exif
3740 information contained in the file:
3744 Where tag can be one of the following:
3746 * (print all Exif tags, in keyword=data format)
3747 ! (print all Exif tags, in tag_number data format)
3748 #hhhh (print data for Exif tag #hhhh)
3753 PhotometricInterpretation
3773 PrimaryChromaticities
3776 JPEGInterchangeFormat
3777 JPEGInterchangeFormatLength
3799 ComponentsConfiguration
3800 CompressedBitsPerPixel
3820 InteroperabilityOffset
3822 SpatialFrequencyResponse
3823 FocalPlaneXResolution
3824 FocalPlaneYResolution
3825 FocalPlaneResolutionUnit
3830 SceneType")), { :scrolled => true })
3833 tbl.attach(update_exif_orientation_check = Gtk::CheckButton.new(utf8(_("Update file's EXIF orientation when rotating a picture"))),
3834 0, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3835 tooltips.set_tip(update_exif_orientation_check, utf8(_("When rotating a picture (Alt-Right/Left), also update EXIF orientation in the file itself")), nil)
3836 update_exif_orientation_check.active = $config['rotate-set-exif'] == 'true'
3838 dialog.vbox.show_all
3839 dialog.run { |response|
3840 if response == Gtk::Dialog::RESPONSE_OK
3841 $config['video-viewer'] = from_utf8(video_viewer_entry.text)
3842 $config['image-editor'] = from_utf8(image_editor_entry.text)
3843 $config['browser'] = from_utf8(browser_entry.text)
3844 if smp_check.active?
3845 $config['mproc'] = smp_spin.value.to_i
3847 $config.delete('mproc')
3849 $config['nogestures'] = nogestures_check.active?
3850 $config['deleteondisk'] = deleteondisk_check.active?
3852 $config['convert-enhance'] = from_utf8(enhance_entry.text)
3853 $config['comments-format'] = from_utf8(commentsformat_entry.text.gsub(/'/, ''))
3854 $config['rotate-set-exif'] = update_exif_orientation_check.active?.to_s
3861 if $undo_tb.sensitive?
3862 $redo_tb.sensitive = $redo_mb.sensitive = true
3863 if not more_undoes = UndoHandler.undo($statusbar)
3864 $undo_tb.sensitive = $undo_mb.sensitive = false
3870 if $redo_tb.sensitive?
3871 $undo_tb.sensitive = $undo_mb.sensitive = true
3872 if not more_redoes = UndoHandler.redo($statusbar)
3873 $redo_tb.sensitive = $redo_mb.sensitive = false
3878 def show_one_click_explanation(intro)
3879 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
3881 %s When such a tool is activated
3882 (<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
3883 on a thumbnail will immediately apply the desired action.
3885 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
3886 ") % intro), { :pos_centered => true })
3889 def create_menu_and_toolbar
3892 mb = Gtk::MenuBar.new
3894 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
3895 filesubmenu = Gtk::Menu.new
3896 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
3897 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
3898 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3899 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
3900 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
3901 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3902 tooltips = Gtk::Tooltips.new
3903 filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
3904 $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3905 tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
3906 filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
3907 $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3908 tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
3909 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
3910 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3911 tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums (anywhere)")), nil)
3912 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3913 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
3914 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3915 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
3916 filesubmenu.append($view_wa = Gtk::ImageMenuItem.new(utf8(_("View web-album with browser"))).set_sensitive(false))
3917 $view_wa.image = Gtk::Image.new("#{$FPATH}/images/stock-view-webalbum-16.png")
3918 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3919 filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
3920 tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
3921 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3922 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
3923 filemenu.set_submenu(filesubmenu)
3926 new.signal_connect('activate') { new_album }
3927 open.signal_connect('activate') { open_file_popup }
3928 $save.signal_connect('activate') { save_current_file_user }
3929 $save_as.signal_connect('activate') { save_as_do }
3930 $merge_current.signal_connect('activate') { merge_current }
3931 $merge_newsubs.signal_connect('activate') { merge_newsubs }
3932 $merge.signal_connect('activate') { merge }
3933 $generate.signal_connect('activate') {
3935 call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
3936 utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
3938 { :successmsg => $xmldoc.root.attributes['multi-languages'] ?
3939 utf8(_("Your web-album is now ready in directory '%s'.
3940 As multi-languages is activated, you will not be able to view it
3941 comfortably in your browser though.") % $xmldoc.root.attributes['destination']) :
3942 utf8(_("Your web-album is now ready in directory '%s'.
3943 Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
3944 :successmsg_linkurl => $xmldoc.root.attributes['multi-languages'] ? $xmldoc.root.attributes['destination'] :
3945 $xmldoc.root.attributes['destination'] + '/index.html',
3946 :closure_after => proc {
3947 $xmldoc.elements.each('//dir') { |elem|
3948 $modified ||= elem.attributes['already-generated'].nil?
3949 elem.add_attribute('already-generated', 'true')
3951 UndoHandler.cleanup #- prevent save_changes to mark current dir as not already generated
3952 $undo_tb.sensitive = $undo_mb.sensitive = false
3953 $redo_tb.sensitive = $redo_mb.sensitive = false
3955 $generated_outofline = true
3958 $view_wa.signal_connect('activate') {
3959 indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
3960 if File.exists?(indexhtml)
3963 show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
3966 $properties.signal_connect('activate') { properties }