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")
2080 system("mkfifo #{pipe.path}")
2081 cmd += " --info-pipe #{pipe.path}"
2082 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2087 id, exitstatus = Process.waitpid2(pid)
2088 gtk_thread_protect { w8.destroy }
2090 if params[:successmsg]
2091 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2093 if params[:closure_after]
2094 gtk_thread_protect(¶ms[:closure_after])
2096 elsif exitstatus == 15
2097 #- say nothing, user aborted
2099 gtk_thread_protect { show_popup($main_window,
2100 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2106 button.signal_connect('clicked') {
2107 Process.kill('SIGTERM', pid)
2111 def save_changes(*forced)
2112 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2116 $xmldir.delete_attribute('already-generated')
2118 propagate_children = proc { |xmldir|
2119 if xmldir.attributes['subdirs-caption']
2120 xmldir.delete_attribute('already-generated')
2122 xmldir.elements.each('dir') { |element|
2123 propagate_children.call(element)
2127 if $xmldir.child_byname_notattr('dir', 'deleted')
2128 new_title = $subalbums_title.buffer.text
2129 if new_title != $xmldir.attributes['subdirs-caption']
2130 parent = $xmldir.parent
2131 if parent.name == 'dir'
2132 parent.delete_attribute('already-generated')
2134 propagate_children.call($xmldir)
2136 $xmldir.add_attribute('subdirs-caption', new_title)
2137 $xmldir.elements.each('dir') { |element|
2138 if !element.attributes['deleted']
2139 path = element.attributes['path']
2140 newtext = $subalbums_edits[path][:editzone].buffer.text
2141 if element.attributes['subdirs-caption']
2142 if element.attributes['subdirs-caption'] != newtext
2143 propagate_children.call(element)
2145 element.add_attribute('subdirs-caption', newtext)
2146 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2148 if element.attributes['thumbnails-caption'] != newtext
2149 element.delete_attribute('already-generated')
2151 element.add_attribute('thumbnails-caption', newtext)
2152 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2158 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2159 if $xmldir.attributes['thumbnails-caption']
2160 path = $xmldir.attributes['path']
2161 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2163 elsif $xmldir.attributes['thumbnails-caption']
2164 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2167 if $xmldir.attributes['thumbnails-caption']
2168 if edit = $subalbums_edits[$xmldir.attributes['path']]
2169 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2173 #- remove and reinsert elements to reflect new ordering
2176 $xmldir.elements.each { |element|
2177 if element.name == 'image' || element.name == 'video'
2178 saves[element.attributes['filename']] = element.remove
2182 $autotable.current_order.each { |path|
2183 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2184 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2187 saves.each_key { |path|
2188 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2189 chld.add_attribute('deleted', 'true')
2193 def sort_by_exif_date
2197 $xmldir.elements.each { |element|
2198 if element.name == 'image' || element.name == 'video'
2199 current_order << element.attributes['filename']
2203 #- look for EXIF dates
2206 if current_order.size > 20
2208 w.set_transient_for($main_window)
2210 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2211 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2212 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2213 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2214 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2215 vb.pack_end(bottom, false, false)
2217 w.signal_connect('delete-event') { w.destroy }
2218 w.window_position = Gtk::Window::POS_CENTER
2222 b.signal_connect('clicked') { aborted = true }
2224 current_order.each { |f|
2226 if entry2type(f) == 'image'
2228 pb.fraction = i.to_f / current_order.size
2229 Gtk.main_iteration while Gtk.events_pending?
2230 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2232 dates[f] = date_time
2245 current_order.each { |f|
2246 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2248 dates[f] = date_time
2254 $xmldir.elements.each { |element|
2255 if element.name == 'image' || element.name == 'video'
2256 saves[element.attributes['filename']] = element.remove
2260 neworder = smartsort(current_order, dates)
2263 $xmldir.add_element(saves[f].name, saves[f].attributes)
2266 #- let the auto-table reflect new ordering
2270 def remove_all_captions
2273 $autotable.current_order.each { |path|
2274 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2275 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2277 save_undo(_("remove all captions"),
2279 texts.each_key { |key|
2280 $name2widgets[key][:textview].buffer.text = texts[key]
2282 $notebook.set_page(1)
2284 texts.each_key { |key|
2285 $name2widgets[key][:textview].buffer.text = ''
2287 $notebook.set_page(1)
2293 $selected_elements.each_key { |path|
2294 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2300 $selected_elements = {}
2304 $undo_tb.sensitive = $undo_mb.sensitive = false
2305 $redo_tb.sensitive = $redo_mb.sensitive = false
2311 $subalbums_vb.children.each { |chld|
2312 $subalbums_vb.remove(chld)
2314 $subalbums = Gtk::Table.new(0, 0, true)
2315 current_y_sub_albums = 0
2317 $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"])
2318 $subalbums_edits = {}
2319 subalbums_counter = 0
2320 subalbums_edits_bypos = {}
2322 add_subalbum = proc { |xmldir, counter|
2323 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2324 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2325 if xmldir == $xmldir
2326 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2327 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2328 caption = xmldir.attributes['thumbnails-caption']
2329 infotype = 'thumbnails'
2331 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2332 captionfile, caption = find_subalbum_caption_info(xmldir)
2333 infotype = find_subalbum_info_type(xmldir)
2335 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2336 hbox = Gtk::HBox.new
2337 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2339 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2342 my_gen_real_thumbnail = proc {
2343 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2346 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2347 f.add(img = Gtk::Image.new)
2348 my_gen_real_thumbnail.call
2350 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2352 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2353 $subalbums.attach(hbox,
2354 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2356 frame, textview = create_editzone($subalbums_sw, 0, img)
2357 textview.buffer.text = caption
2358 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2359 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2361 change_image = proc {
2362 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2364 Gtk::FileChooser::ACTION_OPEN,
2366 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2367 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2368 fc.transient_for = $main_window
2369 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))
2370 f.add(preview_img = Gtk::Image.new)
2372 fc.signal_connect('update-preview') { |w|
2374 if fc.preview_filename
2375 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2376 fc.preview_widget_active = true
2378 rescue Gdk::PixbufError
2379 fc.preview_widget_active = false
2382 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2384 old_file = captionfile
2385 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2386 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2387 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2388 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2390 new_file = fc.filename
2391 msg 3, "new captionfile is: #{fc.filename}"
2392 perform_changefile = proc {
2393 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2394 $modified_pixbufs.delete(thumbnail_file)
2395 xmldir.delete_attribute("#{infotype}-rotate")
2396 xmldir.delete_attribute("#{infotype}-color-swap")
2397 xmldir.delete_attribute("#{infotype}-enhance")
2398 xmldir.delete_attribute("#{infotype}-seektime")
2399 my_gen_real_thumbnail.call
2401 perform_changefile.call
2403 save_undo(_("change caption file for sub-album"),
2405 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2406 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2407 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2408 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2409 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2410 my_gen_real_thumbnail.call
2411 $notebook.set_page(0)
2413 perform_changefile.call
2414 $notebook.set_page(0)
2422 if File.exists?(thumbnail_file)
2423 File.delete(thumbnail_file)
2425 my_gen_real_thumbnail.call
2428 rotate_and_cleanup = proc { |angle|
2429 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2430 if File.exists?(thumbnail_file)
2431 File.delete(thumbnail_file)
2435 move = proc { |direction|
2438 save_changes('forced')
2439 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2440 if direction == 'up'
2441 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2442 subalbums_edits_bypos[oldpos - 1][:position] += 1
2444 if direction == 'down'
2445 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2446 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2448 if direction == 'top'
2449 for i in 1 .. oldpos - 1
2450 subalbums_edits_bypos[i][:position] += 1
2452 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2454 if direction == 'bottom'
2455 for i in oldpos + 1 .. subalbums_counter
2456 subalbums_edits_bypos[i][:position] -= 1
2458 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2462 $xmldir.elements.each('dir') { |element|
2463 if (!element.attributes['deleted'])
2464 elems << [ element.attributes['path'], element.remove ]
2467 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2468 each { |e| $xmldir.add_element(e[1]) }
2469 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2470 $xmldir.elements.each('descendant::dir') { |elem|
2471 elem.delete_attribute('already-generated')
2474 sel = $albums_tv.selection.selected_rows
2476 populate_subalbums_treeview(false)
2477 $albums_tv.selection.select_path(sel[0])
2480 color_swap_and_cleanup = proc {
2481 perform_color_swap_and_cleanup = proc {
2482 color_swap(xmldir, "#{infotype}-")
2483 my_gen_real_thumbnail.call
2485 perform_color_swap_and_cleanup.call
2487 save_undo(_("color swap"),
2489 perform_color_swap_and_cleanup.call
2490 $notebook.set_page(0)
2492 perform_color_swap_and_cleanup.call
2493 $notebook.set_page(0)
2498 change_seektime_and_cleanup = proc {
2499 if values = ask_new_seektime(xmldir, "#{infotype}-")
2500 perform_change_seektime_and_cleanup = proc { |val|
2501 change_seektime(xmldir, "#{infotype}-", val)
2502 my_gen_real_thumbnail.call
2504 perform_change_seektime_and_cleanup.call(values[:new])
2506 save_undo(_("specify seektime"),
2508 perform_change_seektime_and_cleanup.call(values[:old])
2509 $notebook.set_page(0)
2511 perform_change_seektime_and_cleanup.call(values[:new])
2512 $notebook.set_page(0)
2518 whitebalance_and_cleanup = proc {
2519 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2520 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2521 perform_change_whitebalance_and_cleanup = proc { |val|
2522 change_whitebalance(xmldir, "#{infotype}-", val)
2523 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2524 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2525 if File.exists?(thumbnail_file)
2526 File.delete(thumbnail_file)
2529 perform_change_whitebalance_and_cleanup.call(values[:new])
2531 save_undo(_("fix white balance"),
2533 perform_change_whitebalance_and_cleanup.call(values[:old])
2534 $notebook.set_page(0)
2536 perform_change_whitebalance_and_cleanup.call(values[:new])
2537 $notebook.set_page(0)
2543 gammacorrect_and_cleanup = proc {
2544 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2545 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2546 perform_change_gammacorrect_and_cleanup = proc { |val|
2547 change_gammacorrect(xmldir, "#{infotype}-", val)
2548 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2549 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2550 if File.exists?(thumbnail_file)
2551 File.delete(thumbnail_file)
2554 perform_change_gammacorrect_and_cleanup.call(values[:new])
2556 save_undo(_("gamma correction"),
2558 perform_change_gammacorrect_and_cleanup.call(values[:old])
2559 $notebook.set_page(0)
2561 perform_change_gammacorrect_and_cleanup.call(values[:new])
2562 $notebook.set_page(0)
2568 enhance_and_cleanup = proc {
2569 perform_enhance_and_cleanup = proc {
2570 enhance(xmldir, "#{infotype}-")
2571 my_gen_real_thumbnail.call
2574 perform_enhance_and_cleanup.call
2576 save_undo(_("enhance"),
2578 perform_enhance_and_cleanup.call
2579 $notebook.set_page(0)
2581 perform_enhance_and_cleanup.call
2582 $notebook.set_page(0)
2587 evtbox.signal_connect('button-press-event') { |w, event|
2588 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2590 rotate_and_cleanup.call(90)
2592 rotate_and_cleanup.call(-90)
2593 elsif $enhance.active?
2594 enhance_and_cleanup.call
2597 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2598 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2599 { :forbid_left => true, :forbid_right => true,
2600 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2601 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2602 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2603 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2604 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2606 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2611 evtbox.signal_connect('button-press-event') { |w, event|
2612 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2616 evtbox.signal_connect('button-release-event') { |w, event|
2617 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2618 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2619 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2620 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2621 msg 3, "gesture rotate: #{angle}"
2622 rotate_and_cleanup.call(angle)
2625 $gesture_press = nil
2628 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2629 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2630 current_y_sub_albums += 1
2633 if $xmldir.child_byname_notattr('dir', 'deleted')
2635 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2636 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2637 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2638 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2639 #- this album image/caption
2640 if $xmldir.attributes['thumbnails-caption']
2641 add_subalbum.call($xmldir, 0)
2644 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2645 $xmldir.elements.each { |element|
2646 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2647 #- element (image or video) of this album
2648 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2649 msg 3, "dest_img: #{dest_img}"
2650 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2651 total[element.name] += 1
2653 if element.name == 'dir' && !element.attributes['deleted']
2654 #- sub-album image/caption
2655 add_subalbum.call(element, subalbums_counter += 1)
2656 total[element.name] += 1
2659 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2660 total['image'], total['video'], total['dir'] ]))
2661 $subalbums_vb.add($subalbums)
2662 $subalbums_vb.show_all
2664 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2665 $notebook.get_tab_label($autotable_sw).sensitive = false
2666 $notebook.set_page(0)
2667 $thumbnails_title.buffer.text = ''
2669 $notebook.get_tab_label($autotable_sw).sensitive = true
2670 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2673 if !$xmldir.child_byname_notattr('dir', 'deleted')
2674 $notebook.get_tab_label($subalbums_sw).sensitive = false
2675 $notebook.set_page(1)
2677 $notebook.get_tab_label($subalbums_sw).sensitive = true
2681 def pixbuf_or_nil(filename)
2683 return Gdk::Pixbuf.new(filename)
2689 def theme_choose(current)
2690 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2692 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2693 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2694 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2696 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2697 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2698 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2699 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2700 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2701 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2702 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2703 treeview.signal_connect('button-press-event') { |w, event|
2704 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2705 dialog.response(Gtk::Dialog::RESPONSE_OK)
2709 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2711 ([ $FPATH + '/themes/simple' ] + (`find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.find_all { |e| e !~ /simple$/ }.sort)).each { |dir|
2714 iter[0] = File.basename(dir)
2715 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2716 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2717 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2718 if File.basename(dir) == current
2719 treeview.selection.select_iter(iter)
2722 dialog.set_default_size(-1, 500)
2723 dialog.vbox.show_all
2725 dialog.run { |response|
2726 iter = treeview.selection.selected
2728 if response == Gtk::Dialog::RESPONSE_OK && iter
2729 return model.get_value(iter, 0)
2735 def show_password_protections
2736 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2737 child_iter = $albums_iters[xmldir.attributes['path']]
2738 if xmldir.attributes['password-protect']
2739 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2740 already_protected = true
2741 elsif already_protected
2742 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2744 pix = pix.saturate_and_pixelate(1, true)
2750 xmldir.elements.each('dir') { |elem|
2751 if !elem.attributes['deleted']
2752 examine_dir_elem.call(child_iter, elem, already_protected)
2756 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2759 def populate_subalbums_treeview(select_first)
2763 $subalbums_vb.children.each { |chld|
2764 $subalbums_vb.remove(chld)
2767 source = $xmldoc.root.attributes['source']
2768 msg 3, "source: #{source}"
2770 xmldir = $xmldoc.elements['//dir']
2771 if !xmldir || xmldir.attributes['path'] != source
2772 msg 1, _("Corrupted booh file...")
2776 append_dir_elem = proc { |parent_iter, xmldir|
2777 child_iter = $albums_ts.append(parent_iter)
2778 child_iter[0] = File.basename(xmldir.attributes['path'])
2779 child_iter[1] = xmldir.attributes['path']
2780 $albums_iters[xmldir.attributes['path']] = child_iter
2781 msg 3, "puttin location: #{xmldir.attributes['path']}"
2782 xmldir.elements.each('dir') { |elem|
2783 if !elem.attributes['deleted']
2784 append_dir_elem.call(child_iter, elem)
2788 append_dir_elem.call(nil, xmldir)
2789 show_password_protections
2791 $albums_tv.expand_all
2793 $albums_tv.selection.select_iter($albums_ts.iter_first)
2797 def select_current_theme
2798 select_theme($xmldoc.root.attributes['theme'],
2799 $xmldoc.root.attributes['limit-sizes'],
2800 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2801 $xmldoc.root.attributes['thumbnails-per-row'])
2804 def open_file(filename)
2808 $current_path = nil #- invalidate
2809 $modified_pixbufs = {}
2812 $subalbums_vb.children.each { |chld|
2813 $subalbums_vb.remove(chld)
2816 if !File.exists?(filename)
2817 return utf8(_("File not found."))
2821 $xmldoc = REXML::Document.new File.new(filename)
2826 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2827 if entry2type(filename).nil?
2828 return utf8(_("Not a booh file!"))
2830 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."))
2834 if !source = $xmldoc.root.attributes['source']
2835 return utf8(_("Corrupted booh file..."))
2838 if !dest = $xmldoc.root.attributes['destination']
2839 return utf8(_("Corrupted booh file..."))
2842 if !theme = $xmldoc.root.attributes['theme']
2843 return utf8(_("Corrupted booh file..."))
2846 if $xmldoc.root.attributes['version'] < '0.9.0'
2847 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2848 mark_document_as_dirty
2849 if $xmldoc.root.attributes['version'] < '0.8.4'
2850 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2851 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2852 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2853 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2854 if old_dest_dir != new_dest_dir
2855 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2857 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2858 xmldir.elements.each { |element|
2859 if %w(image video).include?(element.name) && !element.attributes['deleted']
2860 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2861 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2862 Dir[old_name + '*'].each { |file|
2863 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2864 file != new_file and sys("mv '#{file}' '#{new_file}'")
2867 if element.name == 'dir' && !element.attributes['deleted']
2868 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2869 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2870 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2874 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2878 $xmldoc.root.add_attribute('version', $VERSION)
2881 select_current_theme
2883 $filename = filename
2884 set_mainwindow_title(nil)
2885 $default_size['thumbnails'] =~ /(.*)x(.*)/
2886 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2887 $albums_thumbnail_size =~ /(.*)x(.*)/
2888 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2890 populate_subalbums_treeview(true)
2892 $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
2896 def open_file_user(filename)
2897 result = open_file(filename)
2899 $config['last-opens'] ||= []
2900 if $config['last-opens'][-1] != utf8(filename)
2901 $config['last-opens'] << utf8(filename)
2903 $orig_filename = $filename
2904 tmp = Tempfile.new("boohtemp")
2907 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2909 $tempfiles << $filename << "#{$filename}.backup"
2911 $orig_filename = nil
2917 if !ask_save_modifications(utf8(_("Save this album?")),
2918 utf8(_("Do you want to save the changes to this album?")),
2919 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2922 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2924 Gtk::FileChooser::ACTION_OPEN,
2926 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2927 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2928 fc.set_current_folder(File.expand_path("~/.booh"))
2929 fc.transient_for = $main_window
2932 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2933 push_mousecursor_wait(fc)
2934 msg = open_file_user(fc.filename)
2949 def additional_booh_options
2952 options += "--mproc #{$config['mproc'].to_i} "
2954 options += "--comments-format '#{$config['comments-format']}' "
2955 if $config['transcode-videos']
2956 options += "--transcode-videos '#{$config['transcode-videos']}' "
2961 def ask_multi_languages(value)
2963 spl = value.split(',')
2964 value = [ spl[0..-2], spl[-1] ]
2967 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
2970 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2971 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2973 lbl = Gtk::Label.new
2975 _("You can choose to activate <b>multi-languages</b> support for this web-album
2976 (it will work only if you publish your web-album on an Apache web-server). This will
2977 use the MultiViews feature of Apache; the pages will be served according to the
2978 value of the Accept-Language HTTP header sent by the web browsers, so that people
2979 with different languages preferences will be able to browse your web-album with
2980 navigation in their language (if language is available).
2983 dialog.vbox.add(lbl)
2984 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
2985 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
2986 add(languages = Gtk::Button.new))))
2988 pick_languages = proc {
2989 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
2992 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2993 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2995 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
2996 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
2998 SUPPORTED_LANGUAGES.each { |lang|
2999 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3000 if ! value.nil? && value[0].include?(lang)
3006 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3007 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3008 fallback_language = nil
3009 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3010 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3011 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3012 fbl_rb.active = true
3013 fallback_language = SUPPORTED_LANGUAGES[0]
3015 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3016 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3017 rb.signal_connect('clicked') { fallback_language = lang }
3018 if ! value.nil? && value[1] == lang
3023 dialog2.window_position = Gtk::Window::POS_MOUSE
3027 dialog2.run { |response|
3029 if resp == Gtk::Dialog::RESPONSE_OK
3031 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3032 value[1] = fallback_language
3033 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3040 languages.signal_connect('clicked') {
3043 dialog.window_position = Gtk::Window::POS_MOUSE
3047 rb_yes.active = true
3048 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3050 rb_no.signal_connect('clicked') {
3054 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3067 dialog.run { |response|
3072 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3074 return [ true, nil ]
3076 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3085 if !ask_save_modifications(utf8(_("Save this album?")),
3086 utf8(_("Do you want to save the changes to this album?")),
3087 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3090 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3092 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3093 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3094 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3096 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3097 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3098 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3099 tbl.attach(src = Gtk::Entry.new,
3100 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3101 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3102 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3103 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3104 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3105 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3106 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3107 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3108 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3109 tbl.attach(dest = Gtk::Entry.new,
3110 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3111 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3112 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3113 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3114 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3115 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3116 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3117 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3118 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3120 tooltips = Gtk::Tooltips.new
3121 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3122 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3123 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3124 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3125 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3126 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3127 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)
3128 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3129 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3130 nperpage_model = Gtk::ListStore.new(String, String)
3131 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3132 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3133 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3134 nperpagecombo.set_attributes(crt, { :markup => 0 })
3135 iter = nperpage_model.append
3136 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3138 [ 12, 20, 30, 40, 50 ].each { |v|
3139 iter = nperpage_model.append
3140 iter[0] = iter[1] = v.to_s
3142 nperpagecombo.active = 0
3144 multilanguages_value = nil
3145 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3146 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3147 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)
3148 multilanguages.signal_connect('clicked') {
3149 retval = ask_multi_languages(multilanguages_value)
3151 multilanguages_value = retval[1]
3153 if multilanguages_value
3154 ml_label.text = utf8(_("Multi-languages: enabled."))
3156 ml_label.text = utf8(_("Multi-languages: disabled."))
3159 if $config['default-multi-languages']
3160 multilanguages_value = $config['default-multi-languages']
3161 ml_label.text = utf8(_("Multi-languages: enabled."))
3163 ml_label.text = utf8(_("Multi-languages: disabled."))
3166 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3167 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3168 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)
3169 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3170 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3171 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)
3173 src_nb_calculated_for = ''
3175 process_src_nb = proc {
3176 if src.text != src_nb_calculated_for
3177 src_nb_calculated_for = src.text
3179 Thread.kill(src_nb_thread)
3182 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3183 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3185 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3186 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3187 src_nb_thread = Thread.new {
3188 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3189 total = { 'image' => 0, 'video' => 0, nil => 0 }
3190 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3191 if File.basename(dir) =~ /^\./
3195 Dir.entries(dir.chomp).each { |file|
3196 total[entry2type(file)] += 1
3198 rescue Errno::EACCES, Errno::ENOENT
3202 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3206 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3209 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3215 timeout_src_nb = Gtk.timeout_add(100) {
3219 src_browse.signal_connect('clicked') {
3220 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3222 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3224 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3225 fc.transient_for = $main_window
3226 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3227 src.text = utf8(fc.filename)
3229 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3234 dest_browse.signal_connect('clicked') {
3235 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3237 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3239 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3240 fc.transient_for = $main_window
3241 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3242 dest.text = utf8(fc.filename)
3247 conf_browse.signal_connect('clicked') {
3248 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3250 Gtk::FileChooser::ACTION_SAVE,
3252 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3253 fc.transient_for = $main_window
3254 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3255 fc.set_current_folder(File.expand_path("~/.booh"))
3256 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3257 conf.text = utf8(fc.filename)
3264 recreate_theme_config = proc {
3265 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3267 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3268 $images_size.each { |s|
3269 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3273 tooltips.set_tip(cb, utf8(s['description']), nil)
3274 theme_sizes << { :widget => cb, :value => s['name'] }
3276 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3277 tooltips = Gtk::Tooltips.new
3278 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3279 theme_sizes << { :widget => cb, :value => 'original' }
3282 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3285 $allowed_N_values.each { |n|
3287 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3289 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3291 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3295 nperrows << { :widget => rb, :value => n }
3297 nperrowradios.show_all
3299 recreate_theme_config.call
3301 theme_button.signal_connect('clicked') {
3302 if newtheme = theme_choose(theme_button.label)
3303 theme_button.label = newtheme
3304 recreate_theme_config.call
3308 dialog.vbox.add(frame1)
3309 dialog.vbox.add(frame2)
3315 dialog.run { |response|
3316 if response == Gtk::Dialog::RESPONSE_OK
3317 srcdir = from_utf8_safe(src.text)
3318 destdir = from_utf8_safe(dest.text)
3319 confpath = from_utf8_safe(conf.text)
3320 if src.text != '' && srcdir == ''
3321 show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3323 elsif !File.directory?(srcdir)
3324 show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3326 elsif dest.text != '' && destdir == ''
3327 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3329 elsif destdir != make_dest_filename(destdir)
3330 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3332 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3333 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3334 inside it will be permanently removed before creating the web-album!
3335 Are you sure you want to continue?")), { :okcancel => true })
3337 elsif File.exists?(destdir) && !File.directory?(destdir)
3338 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3340 elsif conf.text == ''
3341 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3343 elsif conf.text != '' && confpath == ''
3344 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3346 elsif File.directory?(confpath)
3347 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3349 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3350 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3352 system("mkdir '#{destdir}'")
3353 if !File.directory?(destdir)
3354 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3366 srcdir = from_utf8(src.text)
3367 destdir = from_utf8(dest.text)
3368 configskel = File.expand_path(from_utf8(conf.text))
3369 theme = theme_button.label
3370 #- some sort of automatic theme preference
3371 $config['default-theme'] = theme
3372 $config['default-multi-languages'] = multilanguages_value
3373 $config['default-optimize32'] = optimize432.active?.to_s
3374 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3375 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3376 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3377 opt432 = optimize432.active?
3378 madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3379 indexlink = indexlinkentry.text.gsub('\'', ''')
3382 Thread.kill(src_nb_thread)
3383 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3386 Gtk.timeout_remove(timeout_src_nb)
3389 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3390 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3391 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3392 (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
3393 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3394 utf8(_("Please wait while scanning source directory...")),
3396 { :closure_after => proc {
3397 open_file_user(configskel)
3398 $main_window.urgency_hint = true
3404 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3406 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3407 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3408 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3410 source = $xmldoc.root.attributes['source']
3411 dest = $xmldoc.root.attributes['destination']
3412 theme = $xmldoc.root.attributes['theme']
3413 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3414 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3415 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3416 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3418 limit_sizes = limit_sizes.split(/,/)
3420 madewith = ($xmldoc.root.attributes['made-with'] || '').gsub(''', '\'')
3421 indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub(''', '\'')
3422 save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3424 tooltips = Gtk::Tooltips.new
3425 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3426 tbl.attach(Gtk::Label.new(utf8(_("Directory of source photos/videos: "))),
3427 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3428 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3429 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3430 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3431 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3432 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3433 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3434 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3435 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3436 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3437 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3439 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3440 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3441 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3442 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3443 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3444 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3445 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)
3446 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3447 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3448 nperpage_model = Gtk::ListStore.new(String, String)
3449 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3450 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3451 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3452 nperpagecombo.set_attributes(crt, { :markup => 0 })
3453 iter = nperpage_model.append
3454 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3456 [ 12, 20, 30, 40, 50 ].each { |v|
3457 iter = nperpage_model.append
3458 iter[0] = iter[1] = v.to_s
3459 if nperpage && nperpage == v.to_s
3460 nperpagecombo.active_iter = iter
3463 if nperpagecombo.active_iter.nil?
3464 nperpagecombo.active = 0
3467 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3468 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3469 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)
3471 if save_multilanguages_value
3472 ml_label.text = utf8(_("Multi-languages: enabled."))
3474 ml_label.text = utf8(_("Multi-languages: disabled."))
3478 multilanguages.signal_connect('clicked') {
3479 retval = ask_multi_languages(save_multilanguages_value)
3481 save_multilanguages_value = retval[1]
3486 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3487 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3489 indexlinkentry.text = indexlink
3491 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)
3492 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3493 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3495 madewithentry.text = madewith
3497 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)
3501 recreate_theme_config = proc {
3502 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3504 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3506 $images_size.each { |s|
3507 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))