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 photos 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 photos 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 = create_window.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
604 the right 'size', since the height of the thumbnail for this image will be similar
605 to the height of other thumbnails.
608 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)")))).
609 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
610 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
611 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
612 spin.signal_connect('value-changed') {
615 dialog.window_position = Gtk::Window::POS_MOUSE
618 spin.value = value.to_f
625 dialog.run { |response|
629 newval = spin.value.to_f
632 if response == Gtk::Dialog::RESPONSE_OK
634 msg 3, "changing panorama amount to #{newval}"
635 return { :old => value, :new => newval }
642 def change_whitebalance(xmlelem, attributes_prefix, value)
644 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
647 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
649 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
650 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
651 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
652 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
653 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
654 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
655 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
656 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
657 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
658 $modified_pixbufs[thumbnail_img] ||= {}
659 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
660 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
662 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
663 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
665 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
668 $modified_pixbufs[thumbnail_img] ||= {}
669 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
671 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
674 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
675 #- init $modified_pixbufs correctly
676 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
678 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
680 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
682 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
683 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
684 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
688 _("You can fix the <b>white balance</b> of the image, if your image is too blue
689 or too yellow because the recorder didn't detect the light correctly. Drag the
690 slider below the image to the left for more blue, to the right for more yellow.
694 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
696 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
698 dialog.window_position = Gtk::Window::POS_MOUSE
702 timeout = Gtk.timeout_add(100) {
703 if hs.value != lastval
706 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
712 dialog.run { |response|
713 Gtk.timeout_remove(timeout)
714 if response == Gtk::Dialog::RESPONSE_OK
716 newval = hs.value.to_s
717 msg 3, "changing white balance to #{newval}"
719 return { :old => value, :new => newval }
722 $modified_pixbufs[thumbnail_img] ||= {}
723 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
724 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
732 def change_gammacorrect(xmlelem, attributes_prefix, value)
734 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
737 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
739 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
740 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
741 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
742 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
743 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
744 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
745 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
746 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
747 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
748 $modified_pixbufs[thumbnail_img] ||= {}
749 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
750 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
752 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
753 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
755 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
758 $modified_pixbufs[thumbnail_img] ||= {}
759 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
761 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
764 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
765 #- init $modified_pixbufs correctly
766 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
768 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
770 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
772 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
773 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
774 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
778 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
779 or too bright. Drag the slider below the image.
783 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
785 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
787 dialog.window_position = Gtk::Window::POS_MOUSE
791 timeout = Gtk.timeout_add(100) {
792 if hs.value != lastval
795 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
801 dialog.run { |response|
802 Gtk.timeout_remove(timeout)
803 if response == Gtk::Dialog::RESPONSE_OK
805 newval = hs.value.to_s
806 msg 3, "gamma correction to #{newval}"
808 return { :old => value, :new => newval }
811 $modified_pixbufs[thumbnail_img] ||= {}
812 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
813 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
821 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
822 if File.exists?(destfile)
823 File.delete(destfile)
825 #- type can be 'element' or 'subdir'
827 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
829 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
833 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
835 push_mousecursor_wait
836 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
839 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
845 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
846 distribute_multiple_call = Proc.new { |action, arg|
847 $selected_elements.each_key { |path|
848 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
850 if possible_actions[:can_multiple] && $selected_elements.length > 0
851 UndoHandler.begin_batch
852 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
853 UndoHandler.end_batch
855 closures[action].call(arg)
857 $selected_elements = {}
860 if optionals.include?('change_image')
861 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
862 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
863 changeimg.signal_connect('activate') { closures[:change].call }
864 menu.append(Gtk::SeparatorMenuItem.new)
866 if !possible_actions[:can_multiple] || $selected_elements.length == 0
869 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
870 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
871 view.signal_connect('activate') { closures[:view].call }
873 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
874 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
875 view.signal_connect('activate') { closures[:view].call }
876 menu.append(Gtk::SeparatorMenuItem.new)
879 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
880 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
881 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
882 exif.signal_connect('activate') { show_popup($main_window,
883 utf8(`exif -m '#{fullpath}'`),
884 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
885 menu.append(Gtk::SeparatorMenuItem.new)
888 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
889 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
890 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
891 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
892 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
893 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
894 if !possible_actions[:can_multiple] || $selected_elements.length == 0
895 menu.append(Gtk::SeparatorMenuItem.new)
896 if !possible_actions[:forbid_left]
897 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
898 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
899 moveleft.signal_connect('activate') { closures[:move].call('left') }
900 if !possible_actions[:can_left]
901 moveleft.sensitive = false
904 if !possible_actions[:forbid_right]
905 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
906 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
907 moveright.signal_connect('activate') { closures[:move].call('right') }
908 if !possible_actions[:can_right]
909 moveright.sensitive = false
912 if optionals.include?('move_top')
913 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
914 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
915 movetop.signal_connect('activate') { closures[:move].call('top') }
916 if !possible_actions[:can_top]
917 movetop.sensitive = false
920 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
921 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
922 moveup.signal_connect('activate') { closures[:move].call('up') }
923 if !possible_actions[:can_up]
924 moveup.sensitive = false
926 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
927 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
928 movedown.signal_connect('activate') { closures[:move].call('down') }
929 if !possible_actions[:can_down]
930 movedown.sensitive = false
932 if optionals.include?('move_bottom')
933 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
934 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
935 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
936 if !possible_actions[:can_bottom]
937 movebottom.sensitive = false
942 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
943 menu.append(Gtk::SeparatorMenuItem.new)
944 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
945 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
946 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
947 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
948 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
949 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
950 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
951 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
952 seektime.signal_connect('activate') {
953 if possible_actions[:can_multiple] && $selected_elements.length > 0
954 if values = ask_new_seektime(nil, '')
955 distribute_multiple_call.call(:seektime, values)
958 closures[:seektime].call
963 menu.append( Gtk::SeparatorMenuItem.new)
964 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
965 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
966 gammacorrect.signal_connect('activate') {
967 if possible_actions[:can_multiple] && $selected_elements.length > 0
968 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
969 distribute_multiple_call.call(:gammacorrect, values)
972 closures[:gammacorrect].call
975 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
976 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
977 whitebalance.signal_connect('activate') {
978 if possible_actions[:can_multiple] && $selected_elements.length > 0
979 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
980 distribute_multiple_call.call(:whitebalance, values)
983 closures[:whitebalance].call
986 if !possible_actions[:can_multiple] || $selected_elements.length == 0
987 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
988 _("Enhance constrast"))))
990 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
992 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
993 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
994 if type == 'image' && possible_actions[:can_panorama]
995 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
996 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
997 panorama.signal_connect('activate') {
998 if possible_actions[:can_multiple] && $selected_elements.length > 0
999 if values = ask_new_pano_amount(nil, '')
1000 distribute_multiple_call.call(:pano, values)
1003 distribute_multiple_call.call(:pano)
1007 menu.append( Gtk::SeparatorMenuItem.new)
1008 if optionals.include?('delete')
1009 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1010 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1011 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1012 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1013 paste_item.signal_connect('activate') { closures[:paste].call }
1014 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1015 clear_item.signal_connect('activate') { $cuts = [] }
1017 paste_item.sensitive = clear_item.sensitive = false
1020 menu.append( Gtk::SeparatorMenuItem.new)
1022 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1023 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1024 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1025 editexternally.signal_connect('activate') {
1026 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1031 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1032 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1033 if optionals.include?('delete')
1034 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1035 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1038 menu.popup(nil, nil, event.button, event.time)
1041 def delete_current_subalbum
1043 sel = $albums_tv.selection.selected_rows
1044 $xmldir.elements.each { |e|
1045 if e.name == 'image' || e.name == 'video'
1046 e.add_attribute('deleted', 'true')
1049 #- branch if we have a non deleted subalbum
1050 if $xmldir.child_byname_notattr('dir', 'deleted')
1051 $xmldir.delete_attribute('thumbnails-caption')
1052 $xmldir.delete_attribute('thumbnails-captionfile')
1054 $xmldir.add_attribute('deleted', 'true')
1056 while moveup.parent.name == 'dir'
1057 moveup = moveup.parent
1058 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1059 moveup.add_attribute('deleted', 'true')
1066 save_changes('forced')
1067 populate_subalbums_treeview(false)
1068 $albums_tv.selection.select_path(sel[0])
1074 $current_path = nil #- prevent save_changes from being rerun again
1075 sel = $albums_tv.selection.selected_rows
1076 restore_one = proc { |xmldir|
1077 xmldir.elements.each { |e|
1078 if e.name == 'dir' && e.attributes['deleted']
1081 e.delete_attribute('deleted')
1084 restore_one.call($xmldir)
1085 populate_subalbums_treeview(false)
1086 $albums_tv.selection.select_path(sel[0])
1089 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1092 frame1 = Gtk::Frame.new
1093 fullpath = from_utf8("#{$current_path}/#{filename}")
1095 my_gen_real_thumbnail = proc {
1096 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1100 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1101 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1102 pack_start(img = Gtk::Image.new).
1103 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1104 px, mask = pxb.render_pixmap_and_mask(0)
1105 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1106 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1108 frame1.add(img = Gtk::Image.new)
1111 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1112 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1113 my_gen_real_thumbnail.call
1115 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1118 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1120 tooltips = Gtk::Tooltips.new
1121 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1122 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1124 frame2, textview = create_editzone($autotable_sw, 1, img)
1125 textview.buffer.text = caption
1126 textview.set_justification(Gtk::Justification::CENTER)
1128 vbox = Gtk::VBox.new(false, 5)
1129 vbox.pack_start(evtbox, false, false)
1130 vbox.pack_start(frame2, false, false)
1131 autotable.append(vbox, filename)
1133 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1134 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1136 #- to be able to find widgets by name
1137 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1139 cleanup_all_thumbnails = proc {
1140 #- remove out of sync images
1141 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1142 for sizeobj in $images_size
1143 #- cannot use sizeobj because panoramic images will have a larger width
1144 Dir.glob("#{dest_img_base}-*.jpg") do |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 set_mainwindow_title(progress)
1953 filename = $orig_filename || $filename
1956 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
1958 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
1962 $main_window.title = 'booh - ' + File.basename(filename)
1964 $main_window.title = 'booh'
1969 def backend_wait_message(parent, msg, infopipe_path, mode)
1971 w.set_transient_for(parent)
1974 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1975 vb.pack_start(Gtk::Label.new(msg), false, false)
1977 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1978 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
1979 if mode != 'one dir scan'
1980 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1982 if mode == 'web-album'
1983 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1984 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1986 vb.pack_start(Gtk::HSeparator.new, false, false)
1988 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1989 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1990 vb.pack_end(bottom, false, false)
1993 update_progression_title_pb1 = proc {
1994 if mode == 'web-album'
1995 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
1996 elsif mode != 'one dir scan'
1997 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
1999 set_mainwindow_title(pb1_1.fraction)
2003 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2004 refresh_thread = Thread.new {
2005 directories_counter = 0
2006 while line = infopipe.gets
2007 if line =~ /^directories: (\d+), sizes: (\d+)/
2008 directories = $1.to_f + 1
2010 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2011 elements = $3.to_f + 1
2012 if mode == 'web-album'
2016 gtk_thread_protect { pb1_1.fraction = 0 }
2017 if mode != 'one dir scan'
2018 newtext = utf8(full_src_dir_to_rel($1, $2))
2019 newtext = '/' if newtext == ''
2020 gtk_thread_protect { pb1_2.text = newtext }
2021 directories_counter += 1
2022 gtk_thread_protect {
2023 pb1_2.fraction = directories_counter / directories
2024 update_progression_title_pb1.call
2027 elsif line =~ /^processing element$/
2028 element_counter += 1
2029 gtk_thread_protect {
2030 pb1_1.fraction = element_counter / elements
2031 update_progression_title_pb1.call
2033 elsif line =~ /^processing size$/
2034 element_counter += 1
2035 gtk_thread_protect {
2036 pb1_1.fraction = element_counter / elements
2037 update_progression_title_pb1.call
2039 elsif line =~ /^finished processing sizes$/
2040 gtk_thread_protect { pb1_1.fraction = 1 }
2041 elsif line =~ /^creating index.html$/
2042 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2043 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2044 directories_counter = 0
2045 elsif line =~ /^index.html: (.+)\|(.+)/
2046 newtext = utf8(full_src_dir_to_rel($1, $2))
2047 newtext = '/' if newtext == ''
2048 gtk_thread_protect { pb2.text = newtext }
2049 directories_counter += 1
2050 gtk_thread_protect {
2051 pb2.fraction = directories_counter / directories
2052 set_mainwindow_title(0.9 + pb2.fraction / 10)
2054 elsif line =~ /^die: (.*)$/
2061 w.signal_connect('delete-event') { w.destroy }
2062 w.signal_connect('destroy') {
2063 Thread.kill(refresh_thread)
2064 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2067 File.delete(infopipe_path)
2069 set_mainwindow_title(nil)
2071 w.window_position = Gtk::Window::POS_CENTER
2077 def call_backend(cmd, waitmsg, mode, params)
2078 pipe = Tempfile.new("boohpipe")
2079 Thread.critical = true
2081 system("mkfifo #{pipe.path}")
2082 Thread.critical = false
2083 cmd += " --info-pipe #{pipe.path}"
2084 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2089 id, exitstatus = Process.waitpid2(pid)
2090 gtk_thread_protect { w8.destroy }
2092 if params[:successmsg]
2093 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2095 if params[:closure_after]
2096 gtk_thread_protect(¶ms[:closure_after])
2098 elsif exitstatus == 15
2099 #- say nothing, user aborted
2101 gtk_thread_protect { show_popup($main_window,
2102 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2108 button.signal_connect('clicked') {
2109 Process.kill('SIGTERM', pid)
2113 def save_changes(*forced)
2114 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2118 $xmldir.delete_attribute('already-generated')
2120 propagate_children = proc { |xmldir|
2121 if xmldir.attributes['subdirs-caption']
2122 xmldir.delete_attribute('already-generated')
2124 xmldir.elements.each('dir') { |element|
2125 propagate_children.call(element)
2129 if $xmldir.child_byname_notattr('dir', 'deleted')
2130 new_title = $subalbums_title.buffer.text
2131 if new_title != $xmldir.attributes['subdirs-caption']
2132 parent = $xmldir.parent
2133 if parent.name == 'dir'
2134 parent.delete_attribute('already-generated')
2136 propagate_children.call($xmldir)
2138 $xmldir.add_attribute('subdirs-caption', new_title)
2139 $xmldir.elements.each('dir') { |element|
2140 if !element.attributes['deleted']
2141 path = element.attributes['path']
2142 newtext = $subalbums_edits[path][:editzone].buffer.text
2143 if element.attributes['subdirs-caption']
2144 if element.attributes['subdirs-caption'] != newtext
2145 propagate_children.call(element)
2147 element.add_attribute('subdirs-caption', newtext)
2148 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2150 if element.attributes['thumbnails-caption'] != newtext
2151 element.delete_attribute('already-generated')
2153 element.add_attribute('thumbnails-caption', newtext)
2154 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2160 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2161 if $xmldir.attributes['thumbnails-caption']
2162 path = $xmldir.attributes['path']
2163 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2165 elsif $xmldir.attributes['thumbnails-caption']
2166 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2169 if $xmldir.attributes['thumbnails-caption']
2170 if edit = $subalbums_edits[$xmldir.attributes['path']]
2171 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2175 #- remove and reinsert elements to reflect new ordering
2178 $xmldir.elements.each { |element|
2179 if element.name == 'image' || element.name == 'video'
2180 saves[element.attributes['filename']] = element.remove
2184 $autotable.current_order.each { |path|
2185 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2186 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2189 saves.each_key { |path|
2190 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2191 chld.add_attribute('deleted', 'true')
2195 def sort_by_exif_date
2199 $xmldir.elements.each { |element|
2200 if element.name == 'image' || element.name == 'video'
2201 current_order << element.attributes['filename']
2205 #- look for EXIF dates
2208 if current_order.size > 20
2210 w.set_transient_for($main_window)
2212 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2213 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2214 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2215 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2216 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2217 vb.pack_end(bottom, false, false)
2219 w.signal_connect('delete-event') { w.destroy }
2220 w.window_position = Gtk::Window::POS_CENTER
2224 b.signal_connect('clicked') { aborted = true }
2226 current_order.each { |f|
2228 if entry2type(f) == 'image'
2230 pb.fraction = i.to_f / current_order.size
2231 Gtk.main_iteration while Gtk.events_pending?
2232 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2234 dates[f] = date_time
2247 current_order.each { |f|
2248 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2250 dates[f] = date_time
2256 $xmldir.elements.each { |element|
2257 if element.name == 'image' || element.name == 'video'
2258 saves[element.attributes['filename']] = element.remove
2262 neworder = smartsort(current_order, dates)
2265 $xmldir.add_element(saves[f].name, saves[f].attributes)
2268 #- let the auto-table reflect new ordering
2272 def remove_all_captions
2275 $autotable.current_order.each { |path|
2276 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2277 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2279 save_undo(_("remove all captions"),
2281 texts.each_key { |key|
2282 $name2widgets[key][:textview].buffer.text = texts[key]
2284 $notebook.set_page(1)
2286 texts.each_key { |key|
2287 $name2widgets[key][:textview].buffer.text = ''
2289 $notebook.set_page(1)
2295 $selected_elements.each_key { |path|
2296 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2302 $selected_elements = {}
2306 $undo_tb.sensitive = $undo_mb.sensitive = false
2307 $redo_tb.sensitive = $redo_mb.sensitive = false
2313 $subalbums_vb.children.each { |chld|
2314 $subalbums_vb.remove(chld)
2316 $subalbums = Gtk::Table.new(0, 0, true)
2317 current_y_sub_albums = 0
2319 $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"])
2320 $subalbums_edits = {}
2321 subalbums_counter = 0
2322 subalbums_edits_bypos = {}
2324 add_subalbum = proc { |xmldir, counter|
2325 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2326 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2327 if xmldir == $xmldir
2328 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2329 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2330 caption = xmldir.attributes['thumbnails-caption']
2331 infotype = 'thumbnails'
2333 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2334 captionfile, caption = find_subalbum_caption_info(xmldir)
2335 infotype = find_subalbum_info_type(xmldir)
2337 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2338 hbox = Gtk::HBox.new
2339 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2341 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2344 my_gen_real_thumbnail = proc {
2345 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2348 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2349 f.add(img = Gtk::Image.new)
2350 my_gen_real_thumbnail.call
2352 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2354 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2355 $subalbums.attach(hbox,
2356 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2358 frame, textview = create_editzone($subalbums_sw, 0, img)
2359 textview.buffer.text = caption
2360 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2361 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2363 change_image = proc {
2364 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2366 Gtk::FileChooser::ACTION_OPEN,
2368 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2369 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2370 fc.transient_for = $main_window
2371 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))
2372 f.add(preview_img = Gtk::Image.new)
2374 fc.signal_connect('update-preview') { |w|
2376 if fc.preview_filename
2377 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2378 fc.preview_widget_active = true
2380 rescue Gdk::PixbufError
2381 fc.preview_widget_active = false
2384 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2386 old_file = captionfile
2387 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2388 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2389 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2390 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2392 new_file = fc.filename
2393 msg 3, "new captionfile is: #{fc.filename}"
2394 perform_changefile = proc {
2395 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2396 $modified_pixbufs.delete(thumbnail_file)
2397 xmldir.delete_attribute("#{infotype}-rotate")
2398 xmldir.delete_attribute("#{infotype}-color-swap")
2399 xmldir.delete_attribute("#{infotype}-enhance")
2400 xmldir.delete_attribute("#{infotype}-seektime")
2401 my_gen_real_thumbnail.call
2403 perform_changefile.call
2405 save_undo(_("change caption file for sub-album"),
2407 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2408 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2409 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2410 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2411 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2412 my_gen_real_thumbnail.call
2413 $notebook.set_page(0)
2415 perform_changefile.call
2416 $notebook.set_page(0)
2424 if File.exists?(thumbnail_file)
2425 File.delete(thumbnail_file)
2427 my_gen_real_thumbnail.call
2430 rotate_and_cleanup = proc { |angle|
2431 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2432 if File.exists?(thumbnail_file)
2433 File.delete(thumbnail_file)
2437 move = proc { |direction|
2440 save_changes('forced')
2441 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2442 if direction == 'up'
2443 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2444 subalbums_edits_bypos[oldpos - 1][:position] += 1
2446 if direction == 'down'
2447 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2448 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2450 if direction == 'top'
2451 for i in 1 .. oldpos - 1
2452 subalbums_edits_bypos[i][:position] += 1
2454 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2456 if direction == 'bottom'
2457 for i in oldpos + 1 .. subalbums_counter
2458 subalbums_edits_bypos[i][:position] -= 1
2460 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2464 $xmldir.elements.each('dir') { |element|
2465 if (!element.attributes['deleted'])
2466 elems << [ element.attributes['path'], element.remove ]
2469 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2470 each { |e| $xmldir.add_element(e[1]) }
2471 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2472 $xmldir.elements.each('descendant::dir') { |elem|
2473 elem.delete_attribute('already-generated')
2476 sel = $albums_tv.selection.selected_rows
2478 populate_subalbums_treeview(false)
2479 $albums_tv.selection.select_path(sel[0])
2482 color_swap_and_cleanup = proc {
2483 perform_color_swap_and_cleanup = proc {
2484 color_swap(xmldir, "#{infotype}-")
2485 my_gen_real_thumbnail.call
2487 perform_color_swap_and_cleanup.call
2489 save_undo(_("color swap"),
2491 perform_color_swap_and_cleanup.call
2492 $notebook.set_page(0)
2494 perform_color_swap_and_cleanup.call
2495 $notebook.set_page(0)
2500 change_seektime_and_cleanup = proc {
2501 if values = ask_new_seektime(xmldir, "#{infotype}-")
2502 perform_change_seektime_and_cleanup = proc { |val|
2503 change_seektime(xmldir, "#{infotype}-", val)
2504 my_gen_real_thumbnail.call
2506 perform_change_seektime_and_cleanup.call(values[:new])
2508 save_undo(_("specify seektime"),
2510 perform_change_seektime_and_cleanup.call(values[:old])
2511 $notebook.set_page(0)
2513 perform_change_seektime_and_cleanup.call(values[:new])
2514 $notebook.set_page(0)
2520 whitebalance_and_cleanup = proc {
2521 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2522 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2523 perform_change_whitebalance_and_cleanup = proc { |val|
2524 change_whitebalance(xmldir, "#{infotype}-", val)
2525 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2526 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2527 if File.exists?(thumbnail_file)
2528 File.delete(thumbnail_file)
2531 perform_change_whitebalance_and_cleanup.call(values[:new])
2533 save_undo(_("fix white balance"),
2535 perform_change_whitebalance_and_cleanup.call(values[:old])
2536 $notebook.set_page(0)
2538 perform_change_whitebalance_and_cleanup.call(values[:new])
2539 $notebook.set_page(0)
2545 gammacorrect_and_cleanup = proc {
2546 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2547 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2548 perform_change_gammacorrect_and_cleanup = proc { |val|
2549 change_gammacorrect(xmldir, "#{infotype}-", val)
2550 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2551 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2552 if File.exists?(thumbnail_file)
2553 File.delete(thumbnail_file)
2556 perform_change_gammacorrect_and_cleanup.call(values[:new])
2558 save_undo(_("gamma correction"),
2560 perform_change_gammacorrect_and_cleanup.call(values[:old])
2561 $notebook.set_page(0)
2563 perform_change_gammacorrect_and_cleanup.call(values[:new])
2564 $notebook.set_page(0)
2570 enhance_and_cleanup = proc {
2571 perform_enhance_and_cleanup = proc {
2572 enhance(xmldir, "#{infotype}-")
2573 my_gen_real_thumbnail.call
2576 perform_enhance_and_cleanup.call
2578 save_undo(_("enhance"),
2580 perform_enhance_and_cleanup.call
2581 $notebook.set_page(0)
2583 perform_enhance_and_cleanup.call
2584 $notebook.set_page(0)
2589 evtbox.signal_connect('button-press-event') { |w, event|
2590 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2592 rotate_and_cleanup.call(90)
2594 rotate_and_cleanup.call(-90)
2595 elsif $enhance.active?
2596 enhance_and_cleanup.call
2599 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2600 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2601 { :forbid_left => true, :forbid_right => true,
2602 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2603 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2604 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2605 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2606 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2608 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2613 evtbox.signal_connect('button-press-event') { |w, event|
2614 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2618 evtbox.signal_connect('button-release-event') { |w, event|
2619 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2620 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2621 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2622 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2623 msg 3, "gesture rotate: #{angle}"
2624 rotate_and_cleanup.call(angle)
2627 $gesture_press = nil
2630 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2631 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2632 current_y_sub_albums += 1
2635 if $xmldir.child_byname_notattr('dir', 'deleted')
2637 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2638 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2639 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2640 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2641 #- this album image/caption
2642 if $xmldir.attributes['thumbnails-caption']
2643 add_subalbum.call($xmldir, 0)
2646 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2647 $xmldir.elements.each { |element|
2648 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2649 #- element (image or video) of this album
2650 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2651 msg 3, "dest_img: #{dest_img}"
2652 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2653 total[element.name] += 1
2655 if element.name == 'dir' && !element.attributes['deleted']
2656 #- sub-album image/caption
2657 add_subalbum.call(element, subalbums_counter += 1)
2658 total[element.name] += 1
2661 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2662 total['image'], total['video'], total['dir'] ]))
2663 $subalbums_vb.add($subalbums)
2664 $subalbums_vb.show_all
2666 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2667 $notebook.get_tab_label($autotable_sw).sensitive = false
2668 $notebook.set_page(0)
2669 $thumbnails_title.buffer.text = ''
2671 $notebook.get_tab_label($autotable_sw).sensitive = true
2672 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2675 if !$xmldir.child_byname_notattr('dir', 'deleted')
2676 $notebook.get_tab_label($subalbums_sw).sensitive = false
2677 $notebook.set_page(1)
2679 $notebook.get_tab_label($subalbums_sw).sensitive = true
2683 def pixbuf_or_nil(filename)
2685 return Gdk::Pixbuf.new(filename)
2691 def theme_choose(current)
2692 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2694 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2695 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2696 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2698 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2699 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2700 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2701 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2702 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2703 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2704 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2705 treeview.signal_connect('button-press-event') { |w, event|
2706 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2707 dialog.response(Gtk::Dialog::RESPONSE_OK)
2711 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2713 ([ $FPATH + '/themes/simple' ] + (`find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.find_all { |e| e !~ /simple$/ }.sort)).each { |dir|
2716 iter[0] = File.basename(dir)
2717 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2718 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2719 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2720 if File.basename(dir) == current
2721 treeview.selection.select_iter(iter)
2724 dialog.set_default_size(-1, 500)
2725 dialog.vbox.show_all
2727 dialog.run { |response|
2728 iter = treeview.selection.selected
2730 if response == Gtk::Dialog::RESPONSE_OK && iter
2731 return model.get_value(iter, 0)
2737 def show_password_protections
2738 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2739 child_iter = $albums_iters[xmldir.attributes['path']]
2740 if xmldir.attributes['password-protect']
2741 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2742 already_protected = true
2743 elsif already_protected
2744 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2746 pix = pix.saturate_and_pixelate(1, true)
2752 xmldir.elements.each('dir') { |elem|
2753 if !elem.attributes['deleted']
2754 examine_dir_elem.call(child_iter, elem, already_protected)
2758 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2761 def populate_subalbums_treeview(select_first)
2765 $subalbums_vb.children.each { |chld|
2766 $subalbums_vb.remove(chld)
2769 source = $xmldoc.root.attributes['source']
2770 msg 3, "source: #{source}"
2772 xmldir = $xmldoc.elements['//dir']
2773 if !xmldir || xmldir.attributes['path'] != source
2774 msg 1, _("Corrupted booh file...")
2778 append_dir_elem = proc { |parent_iter, xmldir|
2779 child_iter = $albums_ts.append(parent_iter)
2780 child_iter[0] = File.basename(xmldir.attributes['path'])
2781 child_iter[1] = xmldir.attributes['path']
2782 $albums_iters[xmldir.attributes['path']] = child_iter
2783 msg 3, "puttin location: #{xmldir.attributes['path']}"
2784 xmldir.elements.each('dir') { |elem|
2785 if !elem.attributes['deleted']
2786 append_dir_elem.call(child_iter, elem)
2790 append_dir_elem.call(nil, xmldir)
2791 show_password_protections
2793 $albums_tv.expand_all
2795 $albums_tv.selection.select_iter($albums_ts.iter_first)
2799 def select_current_theme
2800 select_theme($xmldoc.root.attributes['theme'],
2801 $xmldoc.root.attributes['limit-sizes'],
2802 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2803 $xmldoc.root.attributes['thumbnails-per-row'])
2806 def open_file(filename)
2810 $current_path = nil #- invalidate
2811 $modified_pixbufs = {}
2814 $subalbums_vb.children.each { |chld|
2815 $subalbums_vb.remove(chld)
2818 if !File.exists?(filename)
2819 return utf8(_("File not found."))
2823 $xmldoc = REXML::Document.new File.new(filename)
2828 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2829 if entry2type(filename).nil?
2830 return utf8(_("Not a booh file!"))
2832 return utf8(_("Not a booh file!\n\nHint: you cannot import directly a photo or video with File/Open.\nUse File/New to create a new album."))
2836 if !source = $xmldoc.root.attributes['source']
2837 return utf8(_("Corrupted booh file..."))
2840 if !dest = $xmldoc.root.attributes['destination']
2841 return utf8(_("Corrupted booh file..."))
2844 if !theme = $xmldoc.root.attributes['theme']
2845 return utf8(_("Corrupted booh file..."))
2848 if $xmldoc.root.attributes['version'] < '0.9.0'
2849 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2850 mark_document_as_dirty
2851 if $xmldoc.root.attributes['version'] < '0.8.4'
2852 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2853 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2854 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2855 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2856 if old_dest_dir != new_dest_dir
2857 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2859 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2860 xmldir.elements.each { |element|
2861 if %w(image video).include?(element.name) && !element.attributes['deleted']
2862 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2863 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2864 Dir[old_name + '*'].each { |file|
2865 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2866 file != new_file and sys("mv '#{file}' '#{new_file}'")
2869 if element.name == 'dir' && !element.attributes['deleted']
2870 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2871 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2872 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2876 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2880 $xmldoc.root.add_attribute('version', $VERSION)
2883 select_current_theme
2885 $filename = filename
2886 set_mainwindow_title(nil)
2887 $default_size['thumbnails'] =~ /(.*)x(.*)/
2888 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2889 $albums_thumbnail_size =~ /(.*)x(.*)/
2890 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2892 populate_subalbums_treeview(true)
2894 $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
2898 def open_file_user(filename)
2899 result = open_file(filename)
2901 $config['last-opens'] ||= []
2902 if $config['last-opens'][-1] != utf8(filename)
2903 $config['last-opens'] << utf8(filename)
2905 $orig_filename = $filename
2906 $main_window.title = 'booh - ' + File.basename($orig_filename)
2907 tmp = Tempfile.new("boohtemp")
2908 Thread.critical = true
2911 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2912 Thread.critical = false
2914 $tempfiles << $filename << "#{$filename}.backup"
2916 $orig_filename = nil
2922 if !ask_save_modifications(utf8(_("Save this album?")),
2923 utf8(_("Do you want to save the changes to this album?")),
2924 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2927 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2929 Gtk::FileChooser::ACTION_OPEN,
2931 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2932 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2933 fc.set_current_folder(File.expand_path("~/.booh"))
2934 fc.transient_for = $main_window
2937 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2938 push_mousecursor_wait(fc)
2939 msg = open_file_user(fc.filename)
2954 def additional_booh_options
2957 options += "--mproc #{$config['mproc'].to_i} "
2959 options += "--comments-format '#{$config['comments-format']}' "
2960 if $config['transcode-videos']
2961 options += "--transcode-videos '#{$config['transcode-videos']}' "
2966 def ask_multi_languages(value)
2968 spl = value.split(',')
2969 value = [ spl[0..-2], spl[-1] ]
2972 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
2975 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2976 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2978 lbl = Gtk::Label.new
2980 _("You can choose to activate <b>multi-languages</b> support for this web-album
2981 (it will work only if you publish your web-album on an Apache web-server). This will
2982 use the MultiViews feature of Apache; the pages will be served according to the
2983 value of the Accept-Language HTTP header sent by the web browsers, so that people
2984 with different languages preferences will be able to browse your web-album with
2985 navigation in their language (if language is available).
2988 dialog.vbox.add(lbl)
2989 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
2990 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
2991 add(languages = Gtk::Button.new))))
2993 pick_languages = proc {
2994 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
2997 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2998 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3000 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3001 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3003 SUPPORTED_LANGUAGES.each { |lang|
3004 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3005 if ! value.nil? && value[0].include?(lang)
3011 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3012 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3013 fallback_language = nil
3014 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3015 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3016 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3017 fbl_rb.active = true
3018 fallback_language = SUPPORTED_LANGUAGES[0]
3020 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3021 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3022 rb.signal_connect('clicked') { fallback_language = lang }
3023 if ! value.nil? && value[1] == lang
3028 dialog2.window_position = Gtk::Window::POS_MOUSE
3032 dialog2.run { |response|
3034 if resp == Gtk::Dialog::RESPONSE_OK
3036 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3037 value[1] = fallback_language
3038 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3045 languages.signal_connect('clicked') {
3048 dialog.window_position = Gtk::Window::POS_MOUSE
3052 rb_yes.active = true
3053 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3055 rb_no.signal_connect('clicked') {
3059 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3072 dialog.run { |response|
3077 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3079 return [ true, nil ]
3081 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3090 if !ask_save_modifications(utf8(_("Save this album?")),
3091 utf8(_("Do you want to save the changes to this album?")),
3092 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3095 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3097 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3098 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3099 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3101 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3102 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3103 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3104 tbl.attach(src = Gtk::Entry.new,
3105 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3106 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3107 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3108 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3109 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3110 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3111 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3112 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3113 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3114 tbl.attach(dest = Gtk::Entry.new,
3115 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3116 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3117 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3118 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3119 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3120 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3121 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3122 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3123 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3125 tooltips = Gtk::Tooltips.new
3126 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3127 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3128 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3129 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3130 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3131 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3132 tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of photos from point-and-shoot cameras - also called compact cameras - is 4/3, whereas photos from SLR cameras - also called reflex cameras - is 3/2)")), nil)
3133 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3134 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3135 nperpage_model = Gtk::ListStore.new(String, String)
3136 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3137 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3138 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3139 nperpagecombo.set_attributes(crt, { :markup => 0 })
3140 iter = nperpage_model.append
3141 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3143 [ 12, 20, 30, 40, 50 ].each { |v|
3144 iter = nperpage_model.append
3145 iter[0] = iter[1] = v.to_s
3147 nperpagecombo.active = 0
3149 multilanguages_value = nil
3150 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3151 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3152 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)
3153 multilanguages.signal_connect('clicked') {
3154 retval = ask_multi_languages(multilanguages_value)
3156 multilanguages_value = retval[1]
3158 if multilanguages_value
3159 ml_label.text = utf8(_("Multi-languages: enabled."))
3161 ml_label.text = utf8(_("Multi-languages: disabled."))
3164 if $config['default-multi-languages']
3165 multilanguages_value = $config['default-multi-languages']
3166 ml_label.text = utf8(_("Multi-languages: enabled."))
3168 ml_label.text = utf8(_("Multi-languages: disabled."))
3171 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3172 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3173 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)
3174 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3175 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3176 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)
3178 src_nb_calculated_for = ''
3180 process_src_nb = proc {
3181 if src.text != src_nb_calculated_for
3182 src_nb_calculated_for = src.text
3184 Thread.kill(src_nb_thread)
3187 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3188 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3190 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3191 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3192 src_nb_thread = Thread.new {
3193 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3194 total = { 'image' => 0, 'video' => 0, nil => 0 }
3195 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3196 if File.basename(dir) =~ /^\./
3200 Dir.entries(dir.chomp).each { |file|
3201 total[entry2type(file)] += 1
3203 rescue Errno::EACCES, Errno::ENOENT
3207 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3211 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3214 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3220 timeout_src_nb = Gtk.timeout_add(100) {
3224 src_browse.signal_connect('clicked') {
3225 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3227 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3229 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3230 fc.transient_for = $main_window
3231 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3232 src.text = utf8(fc.filename)
3234 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3239 dest_browse.signal_connect('clicked') {
3240 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3242 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3244 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3245 fc.transient_for = $main_window
3246 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3247 dest.text = utf8(fc.filename)
3252 conf_browse.signal_connect('clicked') {
3253 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3255 Gtk::FileChooser::ACTION_SAVE,
3257 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3258 fc.transient_for = $main_window
3259 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3260 fc.set_current_folder(File.expand_path("~/.booh"))
3261 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3262 conf.text = utf8(fc.filename)
3269 recreate_theme_config = proc {
3270 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3272 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3273 $images_size.each { |s|
3274 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3278 tooltips.set_tip(cb, utf8(s['description']), nil)
3279 theme_sizes << { :widget => cb, :value => s['name'] }
3281 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3282 tooltips = Gtk::Tooltips.new
3283 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3284 theme_sizes << { :widget => cb, :value => 'original' }
3287 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3290 $allowed_N_values.each { |n|
3292 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3294 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3296 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3300 nperrows << { :widget => rb, :value => n }
3302 nperrowradios.show_all
3304 recreate_theme_config.call
3306 theme_button.signal_connect('clicked') {
3307 if newtheme = theme_choose(theme_button.label)
3308 theme_button.label = newtheme
3309 recreate_theme_config.call
3313 dialog.vbox.add(frame1)
3314 dialog.vbox.add(frame2)
3320 dialog.run { |response|
3321 if response == Gtk::Dialog::RESPONSE_OK
3322 srcdir = from_utf8_safe(src.text)
3323 destdir = from_utf8_safe(dest.text)
3324 confpath = from_utf8_safe(conf.text)
3325 if src.text != '' && srcdir == ''
3326 show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3328 elsif !File.directory?(srcdir)
3329 show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3331 elsif dest.text != '' && destdir == ''
3332 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3334 elsif destdir != make_dest_filename(destdir)
3335 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3337 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3338 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3339 inside it will be permanently removed before creating the web-album!
3340 Are you sure you want to continue?")), { :okcancel => true })
3342 elsif File.exists?(destdir) && !File.directory?(destdir)
3343 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3345 elsif conf.text == ''
3346 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3348 elsif conf.text != '' && confpath == ''
3349 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3351 elsif File.directory?(confpath)
3352 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3354 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3355 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3357 system("mkdir '#{destdir}'")
3358 if !File.directory?(destdir)
3359 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3371 srcdir = from_utf8(src.text)
3372 destdir = from_utf8(dest.text)
3373 configskel = File.expand_path(from_utf8(conf.text))
3374 theme = theme_button.label
3375 #- some sort of automatic theme preference
3376 $config['default-theme'] = theme
3377 $config['default-multi-languages'] = multilanguages_value
3378 $config['default-optimize32'] = optimize432.active?.to_s
3379 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3380 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3381 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3382 opt432 = optimize432.active?
3383 madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3384 indexlink = indexlinkentry.text.gsub('\'', ''')
3387 Thread.kill(src_nb_thread)
3388 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3391 Gtk.timeout_remove(timeout_src_nb)
3394 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3395 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3396 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3397 (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
3398 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3399 utf8(_("Please wait while scanning source directory...")),
3401 { :closure_after => proc {
3402 open_file_user(configskel)
3403 $main_window.urgency_hint = true
3409 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3411 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3412 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3413 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3415 source = $xmldoc.root.attributes['source']
3416 dest = $xmldoc.root.attributes['destination']
3417 theme = $xmldoc.root.attributes['theme']
3418 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3419 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3420 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3421 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3423 limit_sizes = limit_sizes.split(/,/)
3425 madewith = ($xmldoc.root.attributes['made-with'] || '').gsub(''', '\'')
3426 indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub(''', '\'')
3427 save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3429 tooltips = Gtk::Tooltips.new
3430 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3431 tbl.attach(Gtk::Label.new(utf8(_("Directory of source photos/videos: "))),
3432 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3433 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3434 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3435 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3436 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3437 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3438 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3439 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3440 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3441 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3442 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3444 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3445 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3446 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3447 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3448 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3449 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3450 tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of photos from point-and-shoot cameras - also called compact cameras - is 4/3, whereas photos from SLR cameras - also called reflex cameras - is 3/2)")), nil)
3451 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3452 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3453 nperpage_model = Gtk::ListStore.new(String, String)
3454 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3455 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3456 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3457 nperpagecombo.set_attributes(crt, { :markup => 0 })
3458 iter = nperpage_model.append
3459 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3461 [ 12, 20, 30, 40, 50 ].each { |v|
3462 iter = nperpage_model.append
3463 iter[0] = iter[1] = v.to_s
3464 if nperpage && nperpage == v.to_s
3465 nperpagecombo.active_iter = iter
3468 if nperpagecombo.active_iter.nil?
3469 nperpagecombo.active = 0
3472 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3473 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3474 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)
3476 if save_multilanguages_value
3477 ml_label.text = utf8(_("Multi-languages: enabled."))
3479 ml_label.text = utf8(_("Multi-languages: disabled."))
3483 multilanguages.signal_connect('clicked') {
3484 retval = ask_multi_languages(save_multilanguages_value)
3486 save_multilanguages_value = retval[1]
3491 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3492 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3494 indexlinkentry.text = indexlink
3496 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)
3497 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3498 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3500 madewithentry.text = madewith
3502 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)
3506 recreate_theme_config = proc {
3507 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3509 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3511 $images_size.each { |s|
3512 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3514 if limit_sizes.include?(s['name'])
3522 tooltips.set_tip(cb, utf8(s['description']), nil)
3523 theme_sizes << { :widget => cb, :value => s['name'] }
3525 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3526 tooltips = Gtk::Tooltips.new
3527 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3528 if limit_sizes && limit_sizes.include?('original')
3531 theme_sizes << { :widget => cb, :value => 'original' }
3534 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3537 $allowed_N_values.each { |n|
3539 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3541 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3543 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3544 nperrowradios.add(Gtk::Label.new(' '))
3545 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3548 nperrows << { :widget => rb, :value => n.to_s }
3550 nperrowradios.show_all
3552 recreate_theme_config.call
3554 theme_button.signal_connect('clicked') {
3555 if newtheme = theme_choose(theme_button.label)
3558 theme_button.label = newtheme
3559 recreate_theme_config.call
3563 dialog.vbox.add(frame1)
3564 dialog.vbox.add(frame2)
3570 dialog.run { |response|
3571 if response == Gtk::Dialog::RESPONSE_OK
3572 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3573 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3582 save_theme = theme_button.label
3583 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3584 save_opt432 = optimize432.active?
3585 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3586 save_nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3587 save_madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3588 save_indexlink = indexlinkentry.text.gsub('\'', ''')
3591 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)
3592 #- some sort of automatic preferences
3593 if save_theme != theme
3594 $config['default-theme'] = save_theme
3596 if save_multilanguages_value != multilanguages_value
3597 $config['default-multi-languages'] = save_multilanguages_value
3599 if save_opt432 != opt432
3600 $config['default-optimize32'] = save_opt432.to_s
3602 mark_document_as_dirty
3604 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3605 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3606 (save_nperpage ? "--thumbnails-per-page #{save_nperpage} " : '') +
3607 (save_multilanguages_value ? "--multi-languages #{save_multilanguages_value} " : '') +
3608 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3609 utf8(_("Please wait while scanning source directory...")),
3611 { :closure_after => proc {
3612 open_file($filename)
3614 $main_window.urgency_hint = true
3617 #- select_theme merges global variables, need to return to current choices
3618 select_current_theme
3625 sel = $albums_tv.selection.selected_rows
3627 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3628 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3629 utf8(_("Please wait while scanning source directory...")),
3631 { :closure_after => proc {
3632 open_file($filename)
3633 $albums_tv.selection.select_path(sel[0])
3635 $main_window.urgency_hint = true
3642 sel = $albums_tv.selection.selected_rows
3644 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3645 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3646 utf8(_("Please wait while scanning source directory...")),
3648 { :closure_after => proc {
3649 open_file($filename)
3650 $albums_tv.selection.select_path(sel[0])
3652 $main_window.urgency_hint = true
3659 theme = $xmldoc.root.attributes['theme']
3660 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3662 limit_sizes = "--sizes #{limit_sizes}"
3664 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3665 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3666 utf8(_("Please wait while scanning source directory...")),
3668 { :closure_after => proc {
3669 open_file($filename)
3671 $main_window.urgency_hint = true
3676 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3678 Gtk::FileChooser::ACTION_SAVE,
3680 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3681 fc.transient_for = $main_window
3682 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3683 fc.set_current_folder(File.expand_path("~/.booh"))
3684 fc.filename = $orig_filename
3685 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3686 $orig_filename = fc.filename
3687 if ! save_current_file_user
3691 $config['last-opens'] ||= []
3692 $config['last-opens'] << $orig_filename
3698 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3700 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3701 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3702 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3704 dialog.vbox.add(notebook = Gtk::Notebook.new)
3705 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3706 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3707 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3708 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)),
3709 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3710 tooltips = Gtk::Tooltips.new
3711 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3712 for example: /usr/bin/mplayer %f")), nil)
3713 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3714 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3715 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3716 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3717 tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3718 for example: /usr/bin/gimp-remote %f")), nil)
3719 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3720 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3721 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3722 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3723 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3724 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3725 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3726 0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3727 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)),
3728 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3729 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)
3730 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3731 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3732 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)
3733 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original photos/videos as well"))),
3734 0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3735 tooltips.set_tip(deleteondisk_check, utf8(_("Normally, deleting a photo 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)
3737 smp_check.signal_connect('toggled') {
3738 smp_hbox.sensitive = smp_check.active?
3741 smp_check.active = true
3742 smp_spin.value = $config['mproc'].to_i
3744 nogestures_check.active = $config['nogestures']
3745 deleteondisk_check.active = $config['deleteondisk']
3747 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3748 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3749 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3750 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3751 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3752 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nphotos in new albums:"))),
3753 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3754 tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3755 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3756 tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3757 2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3758 tooltips.set_tip(commentsformat_entry, utf8(_("Normally, filenames without extension are used as comments for photos and videos in new albums. Use this entry to use something else.")), nil)
3759 commentsformat_help.signal_connect('clicked') {
3760 show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3761 hence you should look at ImageMagick/identify documentation for the most
3762 accurate and up-to-date documentation. Last time I checked, documentation
3765 Print information about the image in a format of your choosing. You can
3766 include the image filename, type, width, height, Exif data, or other image
3767 attributes by embedding special format characters:
3770 %P page width and height
3774 %e filename extension
3779 %k number of unique colors
3786 %r image class and colorspace
3789 %u unique temporary filename
3802 displays MIFF:bird.miff 512x480 for an image titled bird.miff and whose
3803 width is 512 and height is 480.
3805 If the first character of string is @, the format is read from a file titled
3806 by the remaining characters in the string.
3808 You can also use the following special formatting syntax to print Exif
3809 information contained in the file:
3813 Where tag can be one of the following:
3815 * (print all Exif tags, in keyword=data format)
3816 ! (print all Exif tags, in tag_number data format)
3817 #hhhh (print data for Exif tag #hhhh)
3822 PhotometricInterpretation
3842 PrimaryChromaticities
3845 JPEGInterchangeFormat
3846 JPEGInterchangeFormatLength
3868 ComponentsConfiguration
3869 CompressedBitsPerPixel
3889 InteroperabilityOffset
3891 SpatialFrequencyResponse
3892 FocalPlaneXResolution
3893 FocalPlaneYResolution
3894 FocalPlaneResolutionUnit
3899 SceneType")), { :scrolled => true })
3901 tbl.attach(update_exif_orientation_check = Gtk::CheckButton.new(utf8(_("Update file's EXIF orientation when rotating a picture"))),
3902 0, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3903 tooltips.set_tip(update_exif_orientation_check, utf8(_("When rotating a picture (Alt-Right/Left), also update EXIF orientation in the file itself")), nil)
3904 update_exif_orientation_check.active = $config['rotate-set-exif'] == 'true'
3905 tbl.attach(transcode_videos = Gtk::CheckButton.new(utf8(_("Transcode videos"))).set_active(!$config['transcode-videos'].nil?),
3906 0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3907 transcode_videos.active = ! $config['transcode-videos'].nil?
3908 tbl.attach(transcode_videos_command = Gtk::Entry.new.set_text($config['transcode-videos'] || 'avi:mencoder -nosound -ovc xvid -xvidencopts bitrate=800:me_quality=6 -o %o %f'),
3909 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3910 tooltips.set_tip(transcode_videos, utf8(_("Whether to transcode videos into the web-album instead of using the original videos directly (can be an interesting disk space saver!). First put the extension of the output video and a colon; then use %f to specify the input and %o the output;
3911 for example: avi:mencoder -nosound -ovc xvid -xvidencopts bitrate=800:me_quality=6 -o %o %f")), nil)
3912 transcode_videos.signal_connect('toggled') {
3913 transcode_videos_command.sensitive = transcode_videos.active?
3915 transcode_videos_command.sensitive = transcode_videos.active?
3917 dialog.vbox.show_all
3918 dialog.run { |response|
3919 if response == Gtk::Dialog::RESPONSE_OK
3920 $config['video-viewer'] = from_utf8(video_viewer_entry.text)
3921 $config['image-editor'] = from_utf8(image_editor_entry.text)
3922 $config['browser'] = from_utf8(browser_entry.text)
3923 if smp_check.active?
3924 $config['mproc'] = smp_spin.value.to_i
3926 $config.delete('mproc')
3928 $config['nogestures'] = nogestures_check.active?
3929 $config['deleteondisk'] = deleteondisk_check.active?
3931 $config['convert-enhance'] = from_utf8(enhance_entry.text)
3932 $config['comments-format'] = from_utf8(commentsformat_entry.text.gsub(/'/, ''))
3933 $config['rotate-set-exif'] = update_exif_orientation_check.active?.to_s
3934 if transcode_videos.active?
3935 $config['transcode-videos'] = transcode_videos_command.text
3937 $config.delete('transcode-videos')
3945 if $undo_tb.sensitive?
3946 $redo_tb.sensitive = $redo_mb.sensitive = true
3947 if not more_undoes = UndoHandler.undo($statusbar)
3948 $undo_tb.sensitive = $undo_mb.sensitive = false
3954 if $redo_tb.sensitive?
3955 $undo_tb.sensitive = $undo_mb.sensitive = true
3956 if not more_redoes = UndoHandler.redo($statusbar)
3957 $redo_tb.sensitive = $redo_mb.sensitive = false
3962 def show_one_click_explanation(intro)
3963 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
3965 %s When such a tool is activated
3966 (<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
3967 on a thumbnail will immediately apply the desired action.
3969 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
3970 ") % intro), { :pos_centered => true })
3973 def create_menu_and_toolbar
3976 mb = Gtk::MenuBar.new