5 # A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
13 # Copyright (c) 2004-2008 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
27 require 'booh/libadds'
28 require 'booh/GtkAutoTable'
32 bindtextdomain("booh")
34 require 'booh/rexml/document'
37 require 'booh/booh-lib'
39 require 'booh/UndoHandler'
40 require 'booh/Synchronizator'
45 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
47 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
50 #- default values for some globals
54 $ignore_videos = false
55 $button1_pressed_autotable = false
56 $generated_outofline = false
59 puts _("Usage: %s [OPTION]...") % File.basename($0)
61 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
66 parser = GetoptLong.new
67 parser.set_options(*$options.collect { |ary| ary[0..2] })
69 parser.each_option do |name, arg|
75 when '--verbose-level'
76 $verbose_level = arg.to_i
89 $config_file = File.expand_path('~/.booh-gui-rc')
90 if File.readable?($config_file)
91 $xmldoc = REXML::Document.new(File.new($config_file))
92 $xmldoc.root.elements.each { |element|
93 txt = element.get_text
95 if txt.value =~ /~~~/ || element.name == 'last-opens'
96 $config[element.name] = txt.value.split(/~~~/)
98 $config[element.name] = txt.value
100 elsif element.elements.size == 0
101 $config[element.name] = ''
103 $config[element.name] = {}
104 element.each { |chld|
106 $config[element.name][chld.name] = txt ? txt.value : nil
111 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
112 $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
113 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
114 $config['comments-format'] ||= '%t'
115 if !FileTest.directory?(File.expand_path('~/.booh'))
116 system("mkdir ~/.booh")
118 if $config['mproc'].nil?
120 for line in IO.readlines('/proc/cpuinfo') do
121 line =~ /^processor/ and cpus += 1
124 $config['mproc'] = cpus
127 $config['rotate-set-exif'] ||= 'true'
133 if !system("which convert >/dev/null 2>/dev/null")
134 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
135 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
138 if !system("which identify >/dev/null 2>/dev/null")
139 show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
140 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
142 if !system("which exif >/dev/null 2>/dev/null")
143 show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
145 missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
147 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
150 viewer_binary = $config['video-viewer'].split.first
151 if viewer_binary && !File.executable?(viewer_binary)
152 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
153 You should fix this in Edit/Preferences so that you can view videos.
155 Problem was: '%s' is not an executable file.
156 Hint: don't forget to specify the full path to the executable,
157 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
159 image_editor_binary = $config['image-editor'].split.first
160 if image_editor_binary && !File.executable?(image_editor_binary)
161 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
162 You should fix this in Edit/Preferences so that you can edit images externally.
164 Problem was: '%s' is not an executable file.
165 Hint: don't forget to specify the full path to the executable,
166 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
168 browser_binary = $config['browser'].split.first
169 if browser_binary && !File.executable?(browser_binary)
170 show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
171 You should fix this in Edit/Preferences so that you can open URLs.
173 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
178 if $config['last-opens'] && $config['last-opens'].size > 10
179 $config['last-opens'] = $config['last-opens'][-10, 10]
182 ios = File.open($config_file, "w")
183 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
184 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
185 $config.each_pair { |key, value|
186 elem = $xmldoc.root.add_element key
188 $config[key].each_pair { |subkey, subvalue|
189 subelem = elem.add_element subkey
190 subelem.add_text subvalue.to_s
192 elsif value.is_a? Array
193 elem.add_text value.join('~~~')
198 elem.add_text value.to_s
202 $xmldoc.write(ios, 0)
205 $tempfiles.each { |f|
212 def set_mousecursor(what, *widget)
213 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
214 if widget[0] && widget[0].window
215 widget[0].window.cursor = cursor
217 if $main_window && $main_window.window
218 $main_window.window.cursor = cursor
220 $current_cursor = what
222 def set_mousecursor_wait(*widget)
223 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
224 if Thread.current == Thread.main
225 Gtk.main_iteration while Gtk.events_pending?
228 def set_mousecursor_normal(*widget)
229 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
231 def push_mousecursor_wait(*widget)
232 if $current_cursor != Gdk::Cursor::WATCH
233 $save_cursor = $current_cursor
234 gtk_thread_protect { set_mousecursor_wait(*widget) }
237 def pop_mousecursor(*widget)
238 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
242 source = $xmldoc.root.attributes['source']
243 dest = $xmldoc.root.attributes['destination']
244 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
247 def full_src_dir_to_rel(path, source)
248 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
251 def build_full_dest_filename(filename)
252 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
255 def save_undo(name, closure, *params)
256 UndoHandler.save_undo(name, closure, [ *params ])
257 $undo_tb.sensitive = $undo_mb.sensitive = true
258 $redo_tb.sensitive = $redo_mb.sensitive = false
261 def view_element(filename, closures)
262 if entry2type(filename) == 'video'
263 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
269 w = Gtk::Window.new.set_title(filename)
271 msg 3, "filename: #{filename}"
272 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
273 #- typically this file won't exist in case of videos; try with the largest thumbnail around
274 if !File.exists?(dest_img)
275 if entry2type(filename) == 'video'
276 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
277 if not alternatives.empty?
278 dest_img = alternatives[-1]
281 push_mousecursor_wait
282 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
284 if !File.exists?(dest_img)
285 msg 2, _("Could not generate fullscreen thumbnail!")
290 evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(Gtk::Image.new(dest_img)).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
291 evt.signal_connect('button-press-event') { |this, event|
292 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
293 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
295 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
297 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
298 delete_item.signal_connect('activate') {
300 closures[:delete].call(false)
303 menu.popup(nil, nil, event.button, event.time)
306 evt.signal_connect('button-release-event') { |this, event|
308 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
309 msg 3, "gesture delete: click-drag right button to the bottom"
311 closures[:delete].call(false)
312 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
316 tooltips = Gtk::Tooltips.new
317 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
319 w.signal_connect('key-press-event') { |w,event|
320 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
322 closures[:delete].call(false)
326 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
327 b.signal_connect('clicked') { w.destroy }
330 vb.pack_start(evt, false, false)
331 vb.pack_end(bottom, false, false)
334 w.signal_connect('delete-event') { w.destroy }
335 w.window_position = Gtk::Window::POS_CENTER
339 def scroll_upper(scrolledwindow, ypos_top)
340 newval = scrolledwindow.vadjustment.value -
341 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
342 if newval < scrolledwindow.vadjustment.lower
343 newval = scrolledwindow.vadjustment.lower
345 scrolledwindow.vadjustment.value = newval
348 def scroll_lower(scrolledwindow, ypos_bottom)
349 newval = scrolledwindow.vadjustment.value +
350 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
351 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
352 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
354 scrolledwindow.vadjustment.value = newval
357 def autoscroll_if_needed(scrolledwindow, image, textview)
358 #- autoscroll if cursor or image is not visible, if possible
359 if image && image.window || textview.window
360 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
361 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
362 current_miny_visible = scrolledwindow.vadjustment.value
363 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
364 if ypos_top < current_miny_visible
365 scroll_upper(scrolledwindow, ypos_top)
366 elsif ypos_bottom > current_maxy_visible
367 scroll_lower(scrolledwindow, ypos_bottom)
372 def create_editzone(scrolledwindow, pagenum, image)
373 frame = Gtk::Frame.new
374 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
375 frame.set_shadow_type(Gtk::SHADOW_IN)
376 textview.signal_connect('key-press-event') { |w, event|
377 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
378 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
379 scrolledwindow.signal_emit('key-press-event', event)
381 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
382 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
383 if event.keyval == Gdk::Keyval::GDK_Up
384 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
385 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
387 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
390 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
391 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
393 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
400 candidate_undo_text = nil
401 textview.signal_connect('focus-in-event') { |w, event|
402 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
403 candidate_undo_text = textview.buffer.text
407 textview.signal_connect('key-release-event') { |w, event|
408 if candidate_undo_text && candidate_undo_text != textview.buffer.text
410 save_undo(_("text edit"),
412 save_text = textview.buffer.text
413 textview.buffer.text = text
415 $notebook.set_page(pagenum)
417 textview.buffer.text = save_text
419 $notebook.set_page(pagenum)
421 }, candidate_undo_text)
422 candidate_undo_text = nil
425 if event.state != 0 || ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
426 autoscroll_if_needed(scrolledwindow, image, textview)
431 return [ frame, textview ]
434 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
436 if !$modified_pixbufs[thumbnail_img]
437 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
438 elsif !$modified_pixbufs[thumbnail_img][:orig]
439 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
442 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
445 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
446 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
447 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
448 if pixbuf.height > desired_y
449 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
450 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
451 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
456 if $modified_pixbufs[thumbnail_img][:whitebalance]
457 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
460 #- fix gamma correction
461 if $modified_pixbufs[thumbnail_img][:gammacorrect]
462 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
465 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
468 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
471 #- update rotate attribute
472 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
473 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
475 if $config['rotate-set-exif'] == 'true'
476 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
479 $modified_pixbufs[thumbnail_img] ||= {}
480 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
481 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
483 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
486 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
489 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
491 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
493 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
494 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
496 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
497 $notebook.set_page(0)
498 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
503 def color_swap(xmldir, attributes_prefix)
505 if xmldir.attributes["#{attributes_prefix}color-swap"]
506 xmldir.delete_attribute("#{attributes_prefix}color-swap")
508 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
512 def enhance(xmldir, attributes_prefix)
514 if xmldir.attributes["#{attributes_prefix}enhance"]
515 xmldir.delete_attribute("#{attributes_prefix}enhance")
517 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
521 def change_seektime(xmldir, attributes_prefix, value)
523 xmldir.add_attribute("#{attributes_prefix}seektime", value)
526 def ask_new_seektime(xmldir, attributes_prefix)
528 value = xmldir.attributes["#{attributes_prefix}seektime"]
533 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
535 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
536 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
537 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
541 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
545 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
546 entry.signal_connect('key-press-event') { |w, event|
547 if event.keyval == Gdk::Keyval::GDK_Return
548 dialog.response(Gtk::Dialog::RESPONSE_OK)
550 elsif event.keyval == Gdk::Keyval::GDK_Escape
551 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
554 false #- propagate if needed
558 dialog.window_position = Gtk::Window::POS_MOUSE
561 dialog.run { |response|
564 if response == Gtk::Dialog::RESPONSE_OK
566 msg 3, "changing seektime to #{newval}"
567 return { :old => value, :new => newval }
574 def change_pano_amount(xmldir, attributes_prefix, value)
577 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
579 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
583 def ask_new_pano_amount(xmldir, attributes_prefix)
585 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
590 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
592 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
593 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
594 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
598 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
599 of this panorama image compared to other regular images. For example, if the panorama
600 was taken out of four photos on one row, counting the necessary overlap, the width of
601 this panorama image should probably be roughly three times the width of regular images.
603 With this information, booh will be able to generate panorama thumbnails looking
607 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("none (not a panorama image)")))).
608 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
609 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
610 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
611 spin.signal_connect('value-changed') {
614 dialog.window_position = Gtk::Window::POS_MOUSE
617 spin.value = value.to_f
624 dialog.run { |response|
628 newval = spin.value.to_f
631 if response == Gtk::Dialog::RESPONSE_OK
633 msg 3, "changing panorama amount to #{newval}"
634 return { :old => value, :new => newval }
641 def change_whitebalance(xmlelem, attributes_prefix, value)
643 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
646 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
648 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
649 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
650 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
651 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
652 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
653 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
654 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
655 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
656 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
657 $modified_pixbufs[thumbnail_img] ||= {}
658 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
659 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
661 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
662 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
664 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
667 $modified_pixbufs[thumbnail_img] ||= {}
668 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
670 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
673 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
674 #- init $modified_pixbufs correctly
675 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
677 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
679 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
681 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
682 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
683 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
687 _("You can fix the <b>white balance</b> of the image, if your image is too blue
688 or too yellow because your camera didn't detect the light correctly. Drag the
689 slider below the image to the left for more blue, to the right for more yellow.
693 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
695 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
697 dialog.window_position = Gtk::Window::POS_MOUSE
701 timeout = Gtk.timeout_add(100) {
702 if hs.value != lastval
705 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
711 dialog.run { |response|
712 Gtk.timeout_remove(timeout)
713 if response == Gtk::Dialog::RESPONSE_OK
715 newval = hs.value.to_s
716 msg 3, "changing white balance to #{newval}"
718 return { :old => value, :new => newval }
721 $modified_pixbufs[thumbnail_img] ||= {}
722 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
723 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
731 def change_gammacorrect(xmlelem, attributes_prefix, value)
733 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
736 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
738 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
739 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
740 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
741 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
742 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
743 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
744 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
745 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
746 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
747 $modified_pixbufs[thumbnail_img] ||= {}
748 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
749 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
751 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
752 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
754 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
757 $modified_pixbufs[thumbnail_img] ||= {}
758 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
760 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
763 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
764 #- init $modified_pixbufs correctly
765 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
767 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
769 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
771 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
772 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
773 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
777 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
778 or too bright. Drag the slider below the image.
782 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
784 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
786 dialog.window_position = Gtk::Window::POS_MOUSE
790 timeout = Gtk.timeout_add(100) {
791 if hs.value != lastval
794 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
800 dialog.run { |response|
801 Gtk.timeout_remove(timeout)
802 if response == Gtk::Dialog::RESPONSE_OK
804 newval = hs.value.to_s
805 msg 3, "gamma correction to #{newval}"
807 return { :old => value, :new => newval }
810 $modified_pixbufs[thumbnail_img] ||= {}
811 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
812 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
820 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
821 if File.exists?(destfile)
822 File.delete(destfile)
824 #- type can be 'element' or 'subdir'
826 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
828 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
832 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
834 push_mousecursor_wait
835 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
838 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
844 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
845 distribute_multiple_call = Proc.new { |action, arg|
846 $selected_elements.each_key { |path|
847 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
849 if possible_actions[:can_multiple] && $selected_elements.length > 0
850 UndoHandler.begin_batch
851 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
852 UndoHandler.end_batch
854 closures[action].call(arg)
856 $selected_elements = {}
859 if optionals.include?('change_image')
860 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
861 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
862 changeimg.signal_connect('activate') { closures[:change].call }
863 menu.append(Gtk::SeparatorMenuItem.new)
865 if !possible_actions[:can_multiple] || $selected_elements.length == 0
868 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
869 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
870 view.signal_connect('activate') { closures[:view].call }
872 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
873 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
874 view.signal_connect('activate') { closures[:view].call }
875 menu.append(Gtk::SeparatorMenuItem.new)
878 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
879 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
880 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
881 exif.signal_connect('activate') { show_popup($main_window,
882 utf8(`exif -m '#{fullpath}'`),
883 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
884 menu.append(Gtk::SeparatorMenuItem.new)
887 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
888 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
889 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
890 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
891 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
892 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
893 if !possible_actions[:can_multiple] || $selected_elements.length == 0
894 menu.append(Gtk::SeparatorMenuItem.new)
895 if !possible_actions[:forbid_left]
896 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
897 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
898 moveleft.signal_connect('activate') { closures[:move].call('left') }
899 if !possible_actions[:can_left]
900 moveleft.sensitive = false
903 if !possible_actions[:forbid_right]
904 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
905 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
906 moveright.signal_connect('activate') { closures[:move].call('right') }
907 if !possible_actions[:can_right]
908 moveright.sensitive = false
911 if optionals.include?('move_top')
912 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
913 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
914 movetop.signal_connect('activate') { closures[:move].call('top') }
915 if !possible_actions[:can_top]
916 movetop.sensitive = false
919 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
920 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
921 moveup.signal_connect('activate') { closures[:move].call('up') }
922 if !possible_actions[:can_up]
923 moveup.sensitive = false
925 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
926 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
927 movedown.signal_connect('activate') { closures[:move].call('down') }
928 if !possible_actions[:can_down]
929 movedown.sensitive = false
931 if optionals.include?('move_bottom')
932 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
933 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
934 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
935 if !possible_actions[:can_bottom]
936 movebottom.sensitive = false
941 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
942 menu.append(Gtk::SeparatorMenuItem.new)
943 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
944 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
945 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
946 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
947 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
948 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
949 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
950 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
951 seektime.signal_connect('activate') {
952 if possible_actions[:can_multiple] && $selected_elements.length > 0
953 if values = ask_new_seektime(nil, '')
954 distribute_multiple_call.call(:seektime, values)
957 closures[:seektime].call
962 menu.append( Gtk::SeparatorMenuItem.new)
963 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
964 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
965 gammacorrect.signal_connect('activate') {
966 if possible_actions[:can_multiple] && $selected_elements.length > 0
967 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
968 distribute_multiple_call.call(:gammacorrect, values)
971 closures[:gammacorrect].call
974 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
975 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
976 whitebalance.signal_connect('activate') {
977 if possible_actions[:can_multiple] && $selected_elements.length > 0
978 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
979 distribute_multiple_call.call(:whitebalance, values)
982 closures[:whitebalance].call
985 if !possible_actions[:can_multiple] || $selected_elements.length == 0
986 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
987 _("Enhance constrast"))))
989 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
991 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
992 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
993 if type == 'image' && possible_actions[:can_panorama]
994 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
995 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
996 panorama.signal_connect('activate') {
997 if possible_actions[:can_multiple] && $selected_elements.length > 0
998 if values = ask_new_pano_amount(nil, '')
999 distribute_multiple_call.call(:pano, values)
1002 distribute_multiple_call.call(:pano)
1006 menu.append( Gtk::SeparatorMenuItem.new)
1007 if optionals.include?('delete')
1008 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1009 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1010 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1011 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1012 paste_item.signal_connect('activate') { closures[:paste].call }
1013 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1014 clear_item.signal_connect('activate') { $cuts = [] }
1016 paste_item.sensitive = clear_item.sensitive = false
1019 menu.append( Gtk::SeparatorMenuItem.new)
1021 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1022 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1023 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1024 editexternally.signal_connect('activate') {
1025 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1030 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1031 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1032 if optionals.include?('delete')
1033 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1034 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1037 menu.popup(nil, nil, event.button, event.time)
1040 def delete_current_subalbum
1042 sel = $albums_tv.selection.selected_rows
1043 $xmldir.elements.each { |e|
1044 if e.name == 'image' || e.name == 'video'
1045 e.add_attribute('deleted', 'true')
1048 #- branch if we have a non deleted subalbum
1049 if $xmldir.child_byname_notattr('dir', 'deleted')
1050 $xmldir.delete_attribute('thumbnails-caption')
1051 $xmldir.delete_attribute('thumbnails-captionfile')
1053 $xmldir.add_attribute('deleted', 'true')
1055 while moveup.parent.name == 'dir'
1056 moveup = moveup.parent
1057 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1058 moveup.add_attribute('deleted', 'true')
1065 save_changes('forced')
1066 populate_subalbums_treeview(false)
1067 $albums_tv.selection.select_path(sel[0])
1073 $current_path = nil #- prevent save_changes from being rerun again
1074 sel = $albums_tv.selection.selected_rows
1075 restore_one = proc { |xmldir|
1076 xmldir.elements.each { |e|
1077 if e.name == 'dir' && e.attributes['deleted']
1080 e.delete_attribute('deleted')
1083 restore_one.call($xmldir)
1084 populate_subalbums_treeview(false)
1085 $albums_tv.selection.select_path(sel[0])
1088 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1091 frame1 = Gtk::Frame.new
1092 fullpath = from_utf8("#{$current_path}/#{filename}")
1094 my_gen_real_thumbnail = proc {
1095 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1099 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1100 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1101 pack_start(img = Gtk::Image.new).
1102 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1103 px, mask = pxb.render_pixmap_and_mask(0)
1104 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1105 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1107 frame1.add(img = Gtk::Image.new)
1110 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1111 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1112 my_gen_real_thumbnail.call
1114 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1117 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1119 tooltips = Gtk::Tooltips.new
1120 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1121 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1123 frame2, textview = create_editzone($autotable_sw, 1, img)
1124 textview.buffer.text = caption
1125 textview.set_justification(Gtk::Justification::CENTER)
1127 vbox = Gtk::VBox.new(false, 5)
1128 vbox.pack_start(evtbox, false, false)
1129 vbox.pack_start(frame2, false, false)
1130 autotable.append(vbox, filename)
1132 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1133 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1135 #- to be able to find widgets by name
1136 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1138 cleanup_all_thumbnails = proc {
1139 #- remove out of sync images
1140 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1141 for sizeobj in $images_size
1142 #- cannot use sizeobj because panoramic images will have a larger width
1143 Dir.glob("#{dest_img_base}-*.jpg") do |file|
1151 cleanup_all_thumbnails.call
1152 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1154 $xmldir.delete_attribute('already-generated')
1155 my_gen_real_thumbnail.call
1158 rotate_and_cleanup = proc { |angle|
1159 cleanup_all_thumbnails.call
1160 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1163 move = proc { |direction|
1164 do_method = "move_#{direction}"
1165 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1167 done = autotable.method(do_method).call(vbox)
1168 textview.grab_focus #- because if moving, focus is stolen
1172 save_undo(_("move %s") % direction,
1174 autotable.method(undo_method).call(vbox)
1175 textview.grab_focus #- because if moving, focus is stolen
1176 autoscroll_if_needed($autotable_sw, img, textview)
1177 $notebook.set_page(1)
1179 autotable.method(do_method).call(vbox)
1180 textview.grab_focus #- because if moving, focus is stolen
1181 autoscroll_if_needed($autotable_sw, img, textview)
1182 $notebook.set_page(1)
1188 color_swap_and_cleanup = proc {
1189 perform_color_swap_and_cleanup = proc {
1190 cleanup_all_thumbnails.call
1191 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1192 my_gen_real_thumbnail.call
1195 perform_color_swap_and_cleanup.call
1197 save_undo(_("color swap"),
1199 perform_color_swap_and_cleanup.call
1201 autoscroll_if_needed($autotable_sw, img, textview)
1202 $notebook.set_page(1)
1204 perform_color_swap_and_cleanup.call
1206 autoscroll_if_needed($autotable_sw, img, textview)
1207 $notebook.set_page(1)
1212 change_seektime_and_cleanup_real = proc { |values|
1213 perform_change_seektime_and_cleanup = proc { |val|
1214 cleanup_all_thumbnails.call
1215 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1216 my_gen_real_thumbnail.call
1218 perform_change_seektime_and_cleanup.call(values[:new])
1220 save_undo(_("specify seektime"),
1222 perform_change_seektime_and_cleanup.call(values[:old])
1224 autoscroll_if_needed($autotable_sw, img, textview)
1225 $notebook.set_page(1)
1227 perform_change_seektime_and_cleanup.call(values[:new])
1229 autoscroll_if_needed($autotable_sw, img, textview)
1230 $notebook.set_page(1)
1235 change_seektime_and_cleanup = proc {
1236 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1237 change_seektime_and_cleanup_real.call(values)
1241 change_pano_amount_and_cleanup_real = proc { |values|
1242 perform_change_pano_amount_and_cleanup = proc { |val|
1243 cleanup_all_thumbnails.call
1244 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1246 perform_change_pano_amount_and_cleanup.call(values[:new])
1248 save_undo(_("change panorama amount"),
1250 perform_change_pano_amount_and_cleanup.call(values[:old])
1252 autoscroll_if_needed($autotable_sw, img, textview)
1253 $notebook.set_page(1)
1255 perform_change_pano_amount_and_cleanup.call(values[:new])
1257 autoscroll_if_needed($autotable_sw, img, textview)
1258 $notebook.set_page(1)
1263 change_pano_amount_and_cleanup = proc {
1264 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1265 change_pano_amount_and_cleanup_real.call(values)
1269 whitebalance_and_cleanup_real = proc { |values|
1270 perform_change_whitebalance_and_cleanup = proc { |val|
1271 cleanup_all_thumbnails.call
1272 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1273 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1274 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1276 perform_change_whitebalance_and_cleanup.call(values[:new])
1278 save_undo(_("fix white balance"),
1280 perform_change_whitebalance_and_cleanup.call(values[:old])
1282 autoscroll_if_needed($autotable_sw, img, textview)
1283 $notebook.set_page(1)
1285 perform_change_whitebalance_and_cleanup.call(values[:new])
1287 autoscroll_if_needed($autotable_sw, img, textview)
1288 $notebook.set_page(1)
1293 whitebalance_and_cleanup = proc {
1294 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1295 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1296 whitebalance_and_cleanup_real.call(values)
1300 gammacorrect_and_cleanup_real = proc { |values|
1301 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1302 cleanup_all_thumbnails.call
1303 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1304 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1305 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1307 perform_change_gammacorrect_and_cleanup.call(values[:new])
1309 save_undo(_("gamma correction"),
1311 perform_change_gammacorrect_and_cleanup.call(values[:old])
1313 autoscroll_if_needed($autotable_sw, img, textview)
1314 $notebook.set_page(1)
1316 perform_change_gammacorrect_and_cleanup.call(values[:new])
1318 autoscroll_if_needed($autotable_sw, img, textview)
1319 $notebook.set_page(1)
1324 gammacorrect_and_cleanup = Proc.new {
1325 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1326 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1327 gammacorrect_and_cleanup_real.call(values)
1331 enhance_and_cleanup = proc {
1332 perform_enhance_and_cleanup = proc {
1333 cleanup_all_thumbnails.call
1334 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1335 my_gen_real_thumbnail.call
1338 cleanup_all_thumbnails.call
1339 perform_enhance_and_cleanup.call
1341 save_undo(_("enhance"),
1343 perform_enhance_and_cleanup.call
1345 autoscroll_if_needed($autotable_sw, img, textview)
1346 $notebook.set_page(1)
1348 perform_enhance_and_cleanup.call
1350 autoscroll_if_needed($autotable_sw, img, textview)
1351 $notebook.set_page(1)
1356 delete = proc { |isacut|
1357 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 })
1360 perform_delete = proc {
1361 after = autotable.get_next_widget(vbox)
1363 after = autotable.get_previous_widget(vbox)
1365 if $config['deleteondisk'] && !isacut
1366 msg 3, "scheduling for delete: #{fullpath}"
1367 $todelete << fullpath
1369 autotable.remove_widget(vbox)
1371 $vbox2widgets[after][:textview].grab_focus
1372 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1376 previous_pos = autotable.get_current_number(vbox)
1380 delete_current_subalbum
1382 save_undo(_("delete"),
1384 autotable.reinsert(pos, vbox, filename)
1385 $notebook.set_page(1)
1386 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1388 msg 3, "removing deletion schedule of: #{fullpath}"
1389 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1392 $notebook.set_page(1)
1401 $cuts << { :vbox => vbox, :filename => filename }
1402 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1407 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1410 autotable.queue_draws << proc {
1411 $vbox2widgets[last[:vbox]][:textview].grab_focus
1412 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1414 save_undo(_("paste"),
1416 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1417 $notebook.set_page(1)
1420 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1422 $notebook.set_page(1)
1425 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1430 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1431 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1432 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1433 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1435 textview.signal_connect('key-press-event') { |w, event|
1438 x, y = autotable.get_current_pos(vbox)
1439 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1440 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1441 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1442 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1444 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1445 $vbox2widgets[widget_up][:textview].grab_focus
1452 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1454 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1455 $vbox2widgets[widget_down][:textview].grab_focus
1462 if event.keyval == Gdk::Keyval::GDK_Left
1465 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1472 rotate_and_cleanup.call(-90)
1475 if event.keyval == Gdk::Keyval::GDK_Right
1476 next_ = autotable.get_next_widget(vbox)
1477 if next_ && autotable.get_current_pos(next_)[0] > x
1479 $vbox2widgets[next_][:textview].grab_focus
1486 rotate_and_cleanup.call(90)
1489 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1492 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1493 view_element(filename, { :delete => delete })
1496 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1499 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1503 !propagate #- propagate if needed
1506 $ignore_next_release = false
1507 evtbox.signal_connect('button-press-event') { |w, event|
1508 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1509 if event.state & Gdk::Window::BUTTON3_MASK != 0
1510 #- gesture redo: hold right mouse button then click left mouse button
1511 $config['nogestures'] or perform_redo
1512 $ignore_next_release = true
1514 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1516 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1518 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1519 elsif $enhance.active?
1520 enhance_and_cleanup.call
1521 elsif $delete.active?
1525 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1528 $button1_pressed_autotable = true
1529 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1530 if event.state & Gdk::Window::BUTTON1_MASK != 0
1531 #- gesture undo: hold left mouse button then click right mouse button
1532 $config['nogestures'] or perform_undo
1533 $ignore_next_release = true
1535 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1536 view_element(filename, { :delete => delete })
1541 evtbox.signal_connect('button-release-event') { |w, event|
1542 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1543 if !$ignore_next_release
1544 x, y = autotable.get_current_pos(vbox)
1545 next_ = autotable.get_next_widget(vbox)
1546 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1547 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1548 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1549 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1550 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1551 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1552 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1554 $ignore_next_release = false
1555 $gesture_press = nil
1560 #- handle reordering with drag and drop
1561 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1562 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1563 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1564 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1567 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1569 #- mouse gesture first (dnd disables button-release-event)
1570 if $gesture_press && $gesture_press[:filename] == filename
1571 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1572 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1573 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1574 rotate_and_cleanup.call(angle)
1575 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1577 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1578 msg 3, "gesture delete: click-drag right button to the bottom"
1580 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1585 ctxt.targets.each { |target|
1586 if target.name == 'reorder-elements'
1587 move_dnd = proc { |from,to|
1590 autotable.move(from, to)
1591 save_undo(_("reorder"),
1594 autotable.move(to - 1, from)
1596 autotable.move(to, from + 1)
1598 $notebook.set_page(1)
1600 autotable.move(from, to)
1601 $notebook.set_page(1)
1606 if $multiple_dnd.size == 0
1607 move_dnd.call(selection_data.data.to_i,
1608 autotable.get_current_number(vbox))
1610 UndoHandler.begin_batch
1611 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1613 #- need to update current position between each call
1614 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1615 autotable.get_current_number(vbox))
1617 UndoHandler.end_batch
1628 def create_auto_table
1630 $autotable = Gtk::AutoTable.new(5)
1632 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1633 thumbnails_vb = Gtk::VBox.new(false, 5)
1635 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1636 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1637 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1638 thumbnails_vb.add($autotable)
1640 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1641 $autotable_sw.add_with_viewport(thumbnails_vb)
1643 #- follows stuff for handling multiple elements selection
1644 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1646 update_selected = proc {
1647 $autotable.current_order.each { |path|
1648 w = $name2widgets[path][:evtbox].window
1649 xm = w.position[0] + w.size[0]/2
1650 ym = w.position[1] + w.size[1]/2
1651 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1652 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1653 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1654 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1657 if $selected_elements[path] && ! $selected_elements[path][:keep]
1658 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))
1659 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1660 $selected_elements.delete(path)
1665 $autotable.signal_connect('realize') { |w,e|
1666 gc = Gdk::GC.new($autotable.window)
1667 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1668 gc.function = Gdk::GC::INVERT
1669 #- autoscroll handling for DND and multiple selections
1670 Gtk.timeout_add(100) {
1671 if ! $autotable.window.nil?
1672 w, x, y, mask = $autotable.window.pointer
1673 if mask & Gdk::Window::BUTTON1_MASK != 0
1674 if y < $autotable_sw.vadjustment.value
1676 $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]])
1678 if $button1_pressed_autotable || press_x
1679 scroll_upper($autotable_sw, y)
1682 w, pos_x, pos_y = $autotable.window.pointer
1683 $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]])
1684 update_selected.call
1687 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1689 $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]])
1691 if $button1_pressed_autotable || press_x
1692 scroll_lower($autotable_sw, y)
1695 w, pos_x, pos_y = $autotable.window.pointer
1696 $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]])
1697 update_selected.call
1702 ! $autotable.window.nil?
1706 $autotable.signal_connect('button-press-event') { |w,e|
1708 if !$button1_pressed_autotable
1711 if e.state & Gdk::Window::SHIFT_MASK == 0
1712 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1713 $selected_elements = {}
1714 $statusbar.push(0, utf8(_("Nothing selected.")))
1716 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1718 set_mousecursor(Gdk::Cursor::TCROSS)
1722 $autotable.signal_connect('button-release-event') { |w,e|
1724 if $button1_pressed_autotable
1725 #- unselect all only now
1726 $multiple_dnd = $selected_elements.keys
1727 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1728 $selected_elements = {}
1729 $button1_pressed_autotable = false
1732 $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]])
1733 if $selected_elements.length > 0
1734 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1737 press_x = press_y = pos_x = pos_y = nil
1738 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1742 $autotable.signal_connect('motion-notify-event') { |w,e|
1745 $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]])
1749 $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 update_selected.call
1756 def create_subalbums_page
1758 subalbums_hb = Gtk::HBox.new
1759 $subalbums_vb = Gtk::VBox.new(false, 5)
1760 subalbums_hb.pack_start($subalbums_vb, false, false)
1761 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1762 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1763 $subalbums_sw.add_with_viewport(subalbums_hb)
1766 def save_current_file
1772 ios = File.open($filename, "w")
1773 $xmldoc.write(ios, 0)
1775 rescue Iconv::IllegalSequence
1776 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1777 if ! ios.nil? && ! ios.closed?
1780 $xmldoc.xml_decl.encoding = 'UTF-8'
1781 ios = File.open($filename, "w")
1782 $xmldoc.write(ios, 0)
1793 def save_current_file_user
1794 save_tempfilename = $filename
1795 $filename = $orig_filename
1796 if ! save_current_file
1797 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1798 $filename = save_tempfilename
1802 $generated_outofline = false
1803 $filename = save_tempfilename
1805 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1806 $todelete.each { |f|
1811 def mark_document_as_dirty
1812 $xmldoc.elements.each('//dir') { |elem|
1813 elem.delete_attribute('already-generated')
1817 #- ret: true => ok false => cancel
1818 def ask_save_modifications(msg1, msg2, *options)
1820 options = options.size > 0 ? options[0] : {}
1822 if options[:disallow_cancel]
1823 dialog = Gtk::Dialog.new(msg1,
1825 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1826 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1827 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1829 dialog = Gtk::Dialog.new(msg1,
1831 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1832 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1833 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1834 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1836 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1837 dialog.vbox.add(Gtk::Label.new(msg2))
1838 dialog.window_position = Gtk::Window::POS_CENTER
1841 dialog.run { |response|
1843 if response == Gtk::Dialog::RESPONSE_YES
1844 if ! save_current_file_user
1845 return ask_save_modifications(msg1, msg2, options)
1848 #- if we have generated an album but won't save modifications, we must remove
1849 #- already-generated markers in original file
1850 if $generated_outofline
1852 $xmldoc = REXML::Document.new File.new($orig_filename)
1853 mark_document_as_dirty
1854 ios = File.open($orig_filename, "w")
1855 $xmldoc.write(ios, 0)
1858 puts "exception: #{$!}"
1862 if response == Gtk::Dialog::RESPONSE_CANCEL
1865 $todelete = [] #- unconditionally clear the list of images/videos to delete
1871 def try_quit(*options)
1872 if ask_save_modifications(utf8(_("Save before quitting?")),
1873 utf8(_("Do you want to save your changes before quitting?")),
1879 def show_popup(parent, msg, *options)
1880 dialog = Gtk::Dialog.new
1881 if options[0] && options[0][:title]
1882 dialog.title = options[0][:title]
1884 dialog.title = utf8(_("Booh message"))
1886 lbl = Gtk::Label.new
1887 if options[0] && options[0][:nomarkup]
1892 if options[0] && options[0][:centered]
1893 lbl.set_justify(Gtk::Justification::CENTER)
1895 if options[0] && options[0][:selectable]
1896 lbl.selectable = true
1898 if options[0] && options[0][:topwidget]
1899 dialog.vbox.add(options[0][:topwidget])
1901 if options[0] && options[0][:scrolled]
1902 sw = Gtk::ScrolledWindow.new(nil, nil)
1903 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1904 sw.add_with_viewport(lbl)
1906 dialog.set_default_size(500, 600)
1908 dialog.vbox.add(lbl)
1909 dialog.set_default_size(200, 120)
1911 if options[0] && options[0][:okcancel]
1912 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1914 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1916 if options[0] && options[0][:pos_centered]
1917 dialog.window_position = Gtk::Window::POS_CENTER
1919 dialog.window_position = Gtk::Window::POS_MOUSE
1922 if options[0] && options[0][:linkurl]
1923 linkbut = Gtk::Button.new('')
1924 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1925 linkbut.signal_connect('clicked') {
1926 open_url(options[0][:linkurl])
1927 dialog.response(Gtk::Dialog::RESPONSE_OK)
1928 set_mousecursor_normal
1930 linkbut.relief = Gtk::RELIEF_NONE
1931 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1932 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1933 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1938 if !options[0] || !options[0][:not_transient]
1939 dialog.transient_for = parent
1940 dialog.run { |response|
1942 if options[0] && options[0][:okcancel]
1943 return response == Gtk::Dialog::RESPONSE_OK
1947 dialog.signal_connect('response') { dialog.destroy }
1951 def backend_wait_message(parent, msg, infopipe_path, mode)
1953 w.set_transient_for(parent)
1956 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1957 vb.pack_start(Gtk::Label.new(msg), false, false)
1959 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1960 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1961 if mode != 'one dir scan'
1962 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1964 if mode == 'web-album'
1965 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1966 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1968 vb.pack_start(Gtk::HSeparator.new, false, false)
1970 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1971 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1972 vb.pack_end(bottom, false, false)
1974 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1975 refresh_thread = Thread.new {
1976 directories_counter = 0
1977 while line = infopipe.gets
1978 if line =~ /^directories: (\d+), sizes: (\d+)/
1979 directories = $1.to_f + 1
1981 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1982 elements = $3.to_f + 1
1983 if mode == 'web-album'
1987 gtk_thread_protect { pb1_1.fraction = 0 }
1988 if mode != 'one dir scan'
1989 newtext = utf8(full_src_dir_to_rel($1, $2))
1990 newtext = '/' if newtext == ''
1991 gtk_thread_protect { pb1_2.text = newtext }
1992 directories_counter += 1
1993 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1995 elsif line =~ /^processing element$/
1996 element_counter += 1
1997 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1998 elsif line =~ /^processing size$/
1999 element_counter += 1
2000 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
2001 elsif line =~ /^finished processing sizes$/
2002 gtk_thread_protect { pb1_1.fraction = 1 }
2003 elsif line =~ /^creating index.html$/
2004 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2005 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2006 directories_counter = 0
2007 elsif line =~ /^index.html: (.+)\|(.+)/
2008 newtext = utf8(full_src_dir_to_rel($1, $2))
2009 newtext = '/' if newtext == ''
2010 gtk_thread_protect { pb2.text = newtext }
2011 directories_counter += 1
2012 gtk_thread_protect { pb2.fraction = directories_counter / directories }
2013 elsif line =~ /^die: (.*)$/
2020 w.signal_connect('delete-event') { w.destroy }
2021 w.signal_connect('destroy') {
2022 Thread.kill(refresh_thread)
2023 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2026 File.delete(infopipe_path)
2029 w.window_position = Gtk::Window::POS_CENTER
2035 def call_backend(cmd, waitmsg, mode, params)
2036 pipe = Tempfile.new("boohpipe")
2038 system("mkfifo #{pipe.path}")
2039 cmd += " --info-pipe #{pipe.path}"
2040 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2045 id, exitstatus = Process.waitpid2(pid)
2046 gtk_thread_protect { w8.destroy }
2048 if params[:successmsg]
2049 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2051 if params[:closure_after]
2052 gtk_thread_protect(¶ms[:closure_after])
2054 elsif exitstatus == 15
2055 #- say nothing, user aborted
2057 gtk_thread_protect { show_popup($main_window,
2058 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2064 button.signal_connect('clicked') {
2065 Process.kill('SIGTERM', pid)
2069 def save_changes(*forced)
2070 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2074 $xmldir.delete_attribute('already-generated')
2076 propagate_children = proc { |xmldir|
2077 if xmldir.attributes['subdirs-caption']
2078 xmldir.delete_attribute('already-generated')
2080 xmldir.elements.each('dir') { |element|
2081 propagate_children.call(element)
2085 if $xmldir.child_byname_notattr('dir', 'deleted')
2086 new_title = $subalbums_title.buffer.text
2087 if new_title != $xmldir.attributes['subdirs-caption']
2088 parent = $xmldir.parent
2089 if parent.name == 'dir'
2090 parent.delete_attribute('already-generated')
2092 propagate_children.call($xmldir)
2094 $xmldir.add_attribute('subdirs-caption', new_title)
2095 $xmldir.elements.each('dir') { |element|
2096 if !element.attributes['deleted']
2097 path = element.attributes['path']
2098 newtext = $subalbums_edits[path][:editzone].buffer.text
2099 if element.attributes['subdirs-caption']
2100 if element.attributes['subdirs-caption'] != newtext
2101 propagate_children.call(element)
2103 element.add_attribute('subdirs-caption', newtext)
2104 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2106 if element.attributes['thumbnails-caption'] != newtext
2107 element.delete_attribute('already-generated')
2109 element.add_attribute('thumbnails-caption', newtext)
2110 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2116 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2117 if $xmldir.attributes['thumbnails-caption']
2118 path = $xmldir.attributes['path']
2119 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2121 elsif $xmldir.attributes['thumbnails-caption']
2122 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2125 if $xmldir.attributes['thumbnails-caption']
2126 if edit = $subalbums_edits[$xmldir.attributes['path']]
2127 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2131 #- remove and reinsert elements to reflect new ordering
2134 $xmldir.elements.each { |element|
2135 if element.name == 'image' || element.name == 'video'
2136 saves[element.attributes['filename']] = element.remove
2140 $autotable.current_order.each { |path|
2141 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2142 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2145 saves.each_key { |path|
2146 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2147 chld.add_attribute('deleted', 'true')
2151 def sort_by_exif_date
2155 $xmldir.elements.each { |element|
2156 if element.name == 'image' || element.name == 'video'
2157 current_order << element.attributes['filename']
2161 #- look for EXIF dates
2164 if current_order.size > 20
2166 w.set_transient_for($main_window)
2168 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2169 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2170 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2171 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2172 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2173 vb.pack_end(bottom, false, false)
2175 w.signal_connect('delete-event') { w.destroy }
2176 w.window_position = Gtk::Window::POS_CENTER
2180 b.signal_connect('clicked') { aborted = true }
2182 current_order.each { |f|
2184 if entry2type(f) == 'image'
2186 pb.fraction = i.to_f / current_order.size
2187 Gtk.main_iteration while Gtk.events_pending?
2188 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2190 dates[f] = date_time
2203 current_order.each { |f|
2204 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2206 dates[f] = date_time
2212 $xmldir.elements.each { |element|
2213 if element.name == 'image' || element.name == 'video'
2214 saves[element.attributes['filename']] = element.remove
2218 neworder = smartsort(current_order, dates)
2221 $xmldir.add_element(saves[f].name, saves[f].attributes)
2224 #- let the auto-table reflect new ordering
2228 def remove_all_captions
2231 $autotable.current_order.each { |path|
2232 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2233 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2235 save_undo(_("remove all captions"),
2237 texts.each_key { |key|
2238 $name2widgets[key][:textview].buffer.text = texts[key]
2240 $notebook.set_page(1)
2242 texts.each_key { |key|
2243 $name2widgets[key][:textview].buffer.text = ''
2245 $notebook.set_page(1)
2251 $selected_elements.each_key { |path|
2252 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2258 $selected_elements = {}
2262 $undo_tb.sensitive = $undo_mb.sensitive = false
2263 $redo_tb.sensitive = $redo_mb.sensitive = false
2269 $subalbums_vb.children.each { |chld|
2270 $subalbums_vb.remove(chld)
2272 $subalbums = Gtk::Table.new(0, 0, true)
2273 current_y_sub_albums = 0
2275 $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"])
2276 $subalbums_edits = {}
2277 subalbums_counter = 0
2278 subalbums_edits_bypos = {}
2280 add_subalbum = proc { |xmldir, counter|
2281 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2282 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2283 if xmldir == $xmldir
2284 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2285 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2286 caption = xmldir.attributes['thumbnails-caption']
2287 infotype = 'thumbnails'
2289 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2290 captionfile, caption = find_subalbum_caption_info(xmldir)
2291 infotype = find_subalbum_info_type(xmldir)
2293 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2294 hbox = Gtk::HBox.new
2295 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2297 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2300 my_gen_real_thumbnail = proc {
2301 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2304 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2305 f.add(img = Gtk::Image.new)
2306 my_gen_real_thumbnail.call
2308 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2310 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2311 $subalbums.attach(hbox,
2312 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2314 frame, textview = create_editzone($subalbums_sw, 0, img)
2315 textview.buffer.text = caption
2316 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2317 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2319 change_image = proc {
2320 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2322 Gtk::FileChooser::ACTION_OPEN,
2324 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2325 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2326 fc.transient_for = $main_window
2327 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))
2328 f.add(preview_img = Gtk::Image.new)
2330 fc.signal_connect('update-preview') { |w|
2332 if fc.preview_filename
2333 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2334 fc.preview_widget_active = true
2336 rescue Gdk::PixbufError
2337 fc.preview_widget_active = false
2340 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2342 old_file = captionfile
2343 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2344 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2345 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2346 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2348 new_file = fc.filename
2349 msg 3, "new captionfile is: #{fc.filename}"
2350 perform_changefile = proc {
2351 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2352 $modified_pixbufs.delete(thumbnail_file)
2353 xmldir.delete_attribute("#{infotype}-rotate")
2354 xmldir.delete_attribute("#{infotype}-color-swap")
2355 xmldir.delete_attribute("#{infotype}-enhance")
2356 xmldir.delete_attribute("#{infotype}-seektime")
2357 my_gen_real_thumbnail.call
2359 perform_changefile.call
2361 save_undo(_("change caption file for sub-album"),
2363 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2364 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2365 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2366 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2367 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2368 my_gen_real_thumbnail.call
2369 $notebook.set_page(0)
2371 perform_changefile.call
2372 $notebook.set_page(0)
2380 if File.exists?(thumbnail_file)
2381 File.delete(thumbnail_file)
2383 my_gen_real_thumbnail.call
2386 rotate_and_cleanup = proc { |angle|
2387 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2388 if File.exists?(thumbnail_file)
2389 File.delete(thumbnail_file)
2393 move = proc { |direction|
2396 save_changes('forced')
2397 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2398 if direction == 'up'
2399 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2400 subalbums_edits_bypos[oldpos - 1][:position] += 1
2402 if direction == 'down'
2403 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2404 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2406 if direction == 'top'
2407 for i in 1 .. oldpos - 1
2408 subalbums_edits_bypos[i][:position] += 1
2410 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2412 if direction == 'bottom'
2413 for i in oldpos + 1 .. subalbums_counter
2414 subalbums_edits_bypos[i][:position] -= 1
2416 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2420 $xmldir.elements.each('dir') { |element|
2421 if (!element.attributes['deleted'])
2422 elems << [ element.attributes['path'], element.remove ]
2425 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2426 each { |e| $xmldir.add_element(e[1]) }
2427 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2428 $xmldir.elements.each('descendant::dir') { |elem|
2429 elem.delete_attribute('already-generated')
2432 sel = $albums_tv.selection.selected_rows
2434 populate_subalbums_treeview(false)
2435 $albums_tv.selection.select_path(sel[0])
2438 color_swap_and_cleanup = proc {
2439 perform_color_swap_and_cleanup = proc {
2440 color_swap(xmldir, "#{infotype}-")
2441 my_gen_real_thumbnail.call
2443 perform_color_swap_and_cleanup.call
2445 save_undo(_("color swap"),
2447 perform_color_swap_and_cleanup.call
2448 $notebook.set_page(0)
2450 perform_color_swap_and_cleanup.call
2451 $notebook.set_page(0)
2456 change_seektime_and_cleanup = proc {
2457 if values = ask_new_seektime(xmldir, "#{infotype}-")
2458 perform_change_seektime_and_cleanup = proc { |val|
2459 change_seektime(xmldir, "#{infotype}-", val)
2460 my_gen_real_thumbnail.call
2462 perform_change_seektime_and_cleanup.call(values[:new])
2464 save_undo(_("specify seektime"),
2466 perform_change_seektime_and_cleanup.call(values[:old])
2467 $notebook.set_page(0)
2469 perform_change_seektime_and_cleanup.call(values[:new])
2470 $notebook.set_page(0)
2476 whitebalance_and_cleanup = proc {
2477 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2478 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2479 perform_change_whitebalance_and_cleanup = proc { |val|
2480 change_whitebalance(xmldir, "#{infotype}-", val)
2481 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2482 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2483 if File.exists?(thumbnail_file)
2484 File.delete(thumbnail_file)
2487 perform_change_whitebalance_and_cleanup.call(values[:new])
2489 save_undo(_("fix white balance"),
2491 perform_change_whitebalance_and_cleanup.call(values[:old])
2492 $notebook.set_page(0)
2494 perform_change_whitebalance_and_cleanup.call(values[:new])
2495 $notebook.set_page(0)
2501 gammacorrect_and_cleanup = proc {
2502 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2503 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2504 perform_change_gammacorrect_and_cleanup = proc { |val|
2505 change_gammacorrect(xmldir, "#{infotype}-", val)
2506 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2507 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2508 if File.exists?(thumbnail_file)
2509 File.delete(thumbnail_file)
2512 perform_change_gammacorrect_and_cleanup.call(values[:new])
2514 save_undo(_("gamma correction"),
2516 perform_change_gammacorrect_and_cleanup.call(values[:old])
2517 $notebook.set_page(0)
2519 perform_change_gammacorrect_and_cleanup.call(values[:new])
2520 $notebook.set_page(0)
2526 enhance_and_cleanup = proc {
2527 perform_enhance_and_cleanup = proc {
2528 enhance(xmldir, "#{infotype}-")
2529 my_gen_real_thumbnail.call
2532 perform_enhance_and_cleanup.call
2534 save_undo(_("enhance"),
2536 perform_enhance_and_cleanup.call
2537 $notebook.set_page(0)
2539 perform_enhance_and_cleanup.call
2540 $notebook.set_page(0)
2545 evtbox.signal_connect('button-press-event') { |w, event|
2546 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2548 rotate_and_cleanup.call(90)
2550 rotate_and_cleanup.call(-90)
2551 elsif $enhance.active?
2552 enhance_and_cleanup.call
2555 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2556 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2557 { :forbid_left => true, :forbid_right => true,
2558 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2559 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2560 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2561 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2562 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2564 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2569 evtbox.signal_connect('button-press-event') { |w, event|
2570 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2574 evtbox.signal_connect('button-release-event') { |w, event|
2575 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2576 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2577 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2578 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2579 msg 3, "gesture rotate: #{angle}"
2580 rotate_and_cleanup.call(angle)
2583 $gesture_press = nil
2586 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2587 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2588 current_y_sub_albums += 1
2591 if $xmldir.child_byname_notattr('dir', 'deleted')
2593 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2594 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2595 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2596 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2597 #- this album image/caption
2598 if $xmldir.attributes['thumbnails-caption']
2599 add_subalbum.call($xmldir, 0)
2602 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2603 $xmldir.elements.each { |element|
2604 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2605 #- element (image or video) of this album
2606 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2607 msg 3, "dest_img: #{dest_img}"
2608 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2609 total[element.name] += 1
2611 if element.name == 'dir' && !element.attributes['deleted']
2612 #- sub-album image/caption
2613 add_subalbum.call(element, subalbums_counter += 1)
2614 total[element.name] += 1
2617 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2618 total['image'], total['video'], total['dir'] ]))
2619 $subalbums_vb.add($subalbums)
2620 $subalbums_vb.show_all
2622 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2623 $notebook.get_tab_label($autotable_sw).sensitive = false
2624 $notebook.set_page(0)
2625 $thumbnails_title.buffer.text = ''
2627 $notebook.get_tab_label($autotable_sw).sensitive = true
2628 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2631 if !$xmldir.child_byname_notattr('dir', 'deleted')
2632 $notebook.get_tab_label($subalbums_sw).sensitive = false
2633 $notebook.set_page(1)
2635 $notebook.get_tab_label($subalbums_sw).sensitive = true
2639 def pixbuf_or_nil(filename)
2641 return Gdk::Pixbuf.new(filename)
2647 def theme_choose(current)
2648 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2650 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2651 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2652 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2654 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2655 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2656 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2657 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2658 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2659 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2660 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2661 treeview.signal_connect('button-press-event') { |w, event|
2662 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2663 dialog.response(Gtk::Dialog::RESPONSE_OK)
2667 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2669 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2672 iter[0] = File.basename(dir)
2673 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2674 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2675 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2676 if File.basename(dir) == current
2677 treeview.selection.select_iter(iter)
2681 dialog.set_default_size(700, 400)
2682 dialog.vbox.show_all
2683 dialog.run { |response|
2684 iter = treeview.selection.selected
2686 if response == Gtk::Dialog::RESPONSE_OK && iter
2687 return model.get_value(iter, 0)
2693 def show_password_protections
2694 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2695 child_iter = $albums_iters[xmldir.attributes['path']]
2696 if xmldir.attributes['password-protect']
2697 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2698 already_protected = true
2699 elsif already_protected
2700 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2702 pix = pix.saturate_and_pixelate(1, true)
2708 xmldir.elements.each('dir') { |elem|
2709 if !elem.attributes['deleted']
2710 examine_dir_elem.call(child_iter, elem, already_protected)
2714 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2717 def populate_subalbums_treeview(select_first)
2721 $subalbums_vb.children.each { |chld|
2722 $subalbums_vb.remove(chld)
2725 source = $xmldoc.root.attributes['source']
2726 msg 3, "source: #{source}"
2728 xmldir = $xmldoc.elements['//dir']
2729 if !xmldir || xmldir.attributes['path'] != source
2730 msg 1, _("Corrupted booh file...")
2734 append_dir_elem = proc { |parent_iter, xmldir|
2735 child_iter = $albums_ts.append(parent_iter)
2736 child_iter[0] = File.basename(xmldir.attributes['path'])
2737 child_iter[1] = xmldir.attributes['path']
2738 $albums_iters[xmldir.attributes['path']] = child_iter
2739 msg 3, "puttin location: #{xmldir.attributes['path']}"
2740 xmldir.elements.each('dir') { |elem|
2741 if !elem.attributes['deleted']
2742 append_dir_elem.call(child_iter, elem)
2746 append_dir_elem.call(nil, xmldir)
2747 show_password_protections
2749 $albums_tv.expand_all
2751 $albums_tv.selection.select_iter($albums_ts.iter_first)
2755 def select_current_theme
2756 select_theme($xmldoc.root.attributes['theme'],
2757 $xmldoc.root.attributes['limit-sizes'],
2758 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2759 $xmldoc.root.attributes['thumbnails-per-row'])
2762 def open_file(filename)
2766 $current_path = nil #- invalidate
2767 $modified_pixbufs = {}
2770 $subalbums_vb.children.each { |chld|
2771 $subalbums_vb.remove(chld)
2774 if !File.exists?(filename)
2775 return utf8(_("File not found."))
2779 $xmldoc = REXML::Document.new File.new(filename)
2784 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2785 if entry2type(filename).nil?
2786 return utf8(_("Not a booh file!"))
2788 return utf8(_("Not a booh file!\n\nHint: you cannot import directly an image or video with File/Open.\nUse File/New to create a new album."))
2792 if !source = $xmldoc.root.attributes['source']
2793 return utf8(_("Corrupted booh file..."))
2796 if !dest = $xmldoc.root.attributes['destination']
2797 return utf8(_("Corrupted booh file..."))
2800 if !theme = $xmldoc.root.attributes['theme']
2801 return utf8(_("Corrupted booh file..."))
2804 if $xmldoc.root.attributes['version'] < '0.8.99.2'
2805 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2806 mark_document_as_dirty
2807 if $xmldoc.root.attributes['version'] < '0.8.4'
2808 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2809 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2810 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2811 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2812 if old_dest_dir != new_dest_dir
2813 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2815 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2816 xmldir.elements.each { |element|
2817 if %w(image video).include?(element.name) && !element.attributes['deleted']
2818 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2819 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2820 Dir[old_name + '*'].each { |file|
2821 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2822 file != new_file and sys("mv '#{file}' '#{new_file}'")
2825 if element.name == 'dir' && !element.attributes['deleted']
2826 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2827 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2828 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2832 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2836 $xmldoc.root.add_attribute('version', $VERSION)
2839 select_current_theme
2841 $filename = filename
2842 $default_size['thumbnails'] =~ /(.*)x(.*)/
2843 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2844 $albums_thumbnail_size =~ /(.*)x(.*)/
2845 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2847 populate_subalbums_treeview(true)
2849 $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
2853 def open_file_user(filename)
2854 result = open_file(filename)
2856 $config['last-opens'] ||= []
2857 if $config['last-opens'][-1] != utf8(filename)
2858 $config['last-opens'] << utf8(filename)
2860 $orig_filename = $filename
2861 tmp = Tempfile.new("boohtemp")
2864 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2866 $tempfiles << $filename << "#{$filename}.backup"
2868 $orig_filename = nil
2874 if !ask_save_modifications(utf8(_("Save this album?")),
2875 utf8(_("Do you want to save the changes to this album?")),
2876 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2879 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2881 Gtk::FileChooser::ACTION_OPEN,
2883 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2884 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2885 fc.set_current_folder(File.expand_path("~/.booh"))
2886 fc.transient_for = $main_window
2889 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2890 push_mousecursor_wait(fc)
2891 msg = open_file_user(fc.filename)
2906 def additional_booh_options
2909 options += "--mproc #{$config['mproc'].to_i} "
2911 options += "--comments-format '#{$config['comments-format']}'"
2915 def ask_multi_languages(value)
2917 spl = value.split(',')
2918 value = [ spl[0..-2], spl[-1] ]
2921 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
2924 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2925 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2927 lbl = Gtk::Label.new
2929 _("You can choose to activate <b>multi-languages</b> support for this web-album
2930 (it will work only if you publish your web-album on an Apache web-server). This will
2931 use the MultiViews feature of Apache; the pages will be served according to the
2932 value of the Accept-Language HTTP header sent by the web browsers, so that people
2933 with different languages preferences will be able to browse your web-album with
2934 navigation in their language (if language is available).
2937 dialog.vbox.add(lbl)
2938 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
2939 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
2940 add(languages = Gtk::Button.new))))
2942 pick_languages = proc {
2943 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
2946 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2947 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2949 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
2950 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
2952 SUPPORTED_LANGUAGES.each { |lang|
2953 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
2954 if ! value.nil? && value[0].include?(lang)
2960 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
2961 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
2962 fallback_language = nil
2963 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
2964 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
2965 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
2966 fbl_rb.active = true
2967 fallback_language = SUPPORTED_LANGUAGES[0]
2969 SUPPORTED_LANGUAGES[1..-1].each { |lang|
2970 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
2971 rb.signal_connect('clicked') { fallback_language = lang }
2972 if ! value.nil? && value[1] == lang
2977 dialog2.window_position = Gtk::Window::POS_MOUSE
2981 dialog2.run { |response|
2983 if resp == Gtk::Dialog::RESPONSE_OK
2985 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
2986 value[1] = fallback_language
2987 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| utf8(langname(v)) }.join(', '), utf8(langname(value[1])) ])
2994 languages.signal_connect('clicked') {
2997 dialog.window_position = Gtk::Window::POS_MOUSE
3001 rb_yes.active = true
3002 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| utf8(langname(v)) }.join(', '), utf8(langname(value[1])) ])
3004 rb_no.signal_connect('clicked') {
3008 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3021 dialog.run { |response|
3026 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3028 return [ true, nil ]
3030 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3039 if !ask_save_modifications(utf8(_("Save this album?")),
3040 utf8(_("Do you want to save the changes to this album?")),
3041 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3044 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3046 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3047 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3048 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3050 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3051 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
3052 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3053 tbl.attach(src = Gtk::Entry.new,
3054 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3055 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3056 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3057 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
3058 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3059 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3060 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3061 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3062 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3063 tbl.attach(dest = Gtk::Entry.new,
3064 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3065 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3066 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3067 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3068 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3069 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3070 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3071 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3072 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3074 tooltips = Gtk::Tooltips.new
3075 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3076 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3077 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3078 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3079 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3080 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
3081 tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of pictures from non digital cameras are 3/2 when pictures from digital cameras are 4/3)")), nil)
3082 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3083 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3084 nperpage_model = Gtk::ListStore.new(String, String)
3085 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3086 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3087 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3088 nperpagecombo.set_attributes(crt, { :markup => 0 })
3089 iter = nperpage_model.append
3090 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3092 [ 12, 20, 30, 40, 50 ].each { |v|
3093 iter = nperpage_model.append
3094 iter[0] = iter[1] = v.to_s
3096 nperpagecombo.active = 0
3098 multilanguages_value = nil
3099 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new(utf8(_("Multi-languages: disabled."))), false, false, 0).
3100 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3101 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)
3102 multilanguages.signal_connect('clicked') {
3103 retval = ask_multi_languages(multilanguages_value)
3105 multilanguages_value = retval[1]
3107 if multilanguages_value
3108 ml_label.text = utf8(_("Multi-languages: enabled."))
3110 ml_label.text = utf8(_("Multi-languages: disabled."))
3114 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3115 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3116 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)
3117 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3118 pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
3119 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)
3121 src_nb_calculated_for = ''
3123 process_src_nb = proc {
3124 if src.text != src_nb_calculated_for
3125 src_nb_calculated_for = src.text
3127 Thread.kill(src_nb_thread)
3130 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3131 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3133 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3134 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3135 src_nb_thread = Thread.new {
3136 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3137 total = { 'image' => 0, 'video' => 0, nil => 0 }
3138 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3139 if File.basename(dir) =~ /^\./
3143 Dir.entries(dir.chomp).each { |file|
3144 total[entry2type(file)] += 1
3146 rescue Errno::EACCES, Errno::ENOENT
3150 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3154 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3157 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3163 timeout_src_nb = Gtk.timeout_add(100) {
3167 src_browse.signal_connect('clicked') {
3168 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
3170 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3172 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3173 fc.transient_for = $main_window
3174 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3175 src.text = utf8(fc.filename)
3177 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3182 dest_browse.signal_connect('clicked') {
3183 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3185 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3187 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3188 fc.transient_for = $main_window
3189 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3190 dest.text = utf8(fc.filename)
3195 conf_browse.signal_connect('clicked') {
3196 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3198 Gtk::FileChooser::ACTION_SAVE,
3200 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3201 fc.transient_for = $main_window
3202 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3203 fc.set_current_folder(File.expand_path("~/.booh"))
3204 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3205 conf.text = utf8(fc.filename)
3212 recreate_theme_config = proc {
3213 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3215 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3216 $images_size.each { |s|
3217 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3221 tooltips.set_tip(cb, utf8(s['description']), nil)
3222 theme_sizes << { :widget => cb, :value => s['name'] }
3224 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3225 tooltips = Gtk::Tooltips.new
3226 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3227 theme_sizes << { :widget => cb, :value => 'original' }
3230 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3233 $allowed_N_values.each { |n|
3235 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3237 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3239 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3243 nperrows << { :widget => rb, :value => n }
3245 nperrowradios.show_all
3247 recreate_theme_config.call
3249 theme_button.signal_connect('clicked') {
3250 if newtheme = theme_choose(theme_button.label)
3251 theme_button.label = newtheme
3252 recreate_theme_config.call
3256 dialog.vbox.add(frame1)
3257 dialog.vbox.add(frame2)
3263 dialog.run { |response|
3264 if response == Gtk::Dialog::RESPONSE_OK
3265 srcdir = from_utf8_safe(src.text)
3266 destdir = from_utf8_safe(dest.text)
3267 confpath = from_utf8_safe(conf.text)
3268 if src.text != '' && srcdir == ''
3269 show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3271 elsif !File.directory?(srcdir)
3272 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3274 elsif dest.text != '' && destdir == ''
3275 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3277 elsif destdir != make_dest_filename(destdir)
3278 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3280 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3281 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
3283 elsif File.exists?(destdir) && !File.directory?(destdir)
3284 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3286 elsif conf.text == ''
3287 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3289 elsif conf.text != '' && confpath == ''
3290 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3292 elsif File.directory?(confpath)
3293 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3295 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3296 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3298 system("mkdir '#{destdir}'")
3299 if !File.directory?(destdir)
3300 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3312 srcdir = from_utf8(src.text)
3313 destdir = from_utf8(dest.text)
3314 configskel = File.expand_path(from_utf8(conf.text))
3315 theme = theme_button.label
3316 #- some sort of automatic theme preference
3317 $config['default-theme'] = theme
3318 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3319 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3320 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3321 opt432 = optimize432.active?
3322 madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3323 indexlink = indexlinkentry.text.gsub('\'', ''')
3326 Thread.kill(src_nb_thread)
3327 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3330 Gtk.timeout_remove(timeout_src_nb)
3333 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3334 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3335 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3336 (multilanguages_value ? "--multi-languages #{multilanguages_value}" : '') +
3337 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3338 utf8(_("Please wait while scanning source directory...")),
3340 { :closure_after => proc { open_file_user(configskel) } })
3345 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3347 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3348 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3349 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3351 source = $xmldoc.root.attributes['source']
3352 dest = $xmldoc.root.attributes['destination']
3353 theme = $xmldoc.root.attributes['theme']
3354 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3355 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3356 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3357 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3359 limit_sizes = limit_sizes.split(/,/)
3361 madewith = ($xmldoc.root.attributes['made-with'] || '').gsub(''', '\'')
3362 indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub(''', '\'')
3363 save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3365 tooltips = Gtk::Tooltips.new
3366 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3367 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3368 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3369 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3370 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3371 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3372 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3373 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3374 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3375 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3376 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3377 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3378 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3380 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3381 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3382 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3383 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3384 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3385 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3386 tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of pictures from non digital cameras are 3/2 when pictures from digital cameras are 4/3)")), nil)
3387 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3388 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3389 nperpage_model = Gtk::ListStore.new(String, String)
3390 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3391 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3392 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3393 nperpagecombo.set_attributes(crt, { :markup => 0 })
3394 iter = nperpage_model.append
3395 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3397 [ 12, 20, 30, 40, 50 ].each { |v|
3398 iter = nperpage_model.append
3399 iter[0] = iter[1] = v.to_s
3400 if nperpage && nperpage == v.to_s
3401 nperpagecombo.active_iter = iter
3404 if nperpagecombo.active_iter.nil?
3405 nperpagecombo.active = 0
3408 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3409 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3410 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)
3412 if save_multilanguages_value
3413 ml_label.text = utf8(_("Multi-languages: enabled."))
3415 ml_label.text = utf8(_("Multi-languages: disabled."))
3419 multilanguages.signal_connect('clicked') {
3420 retval = ask_multi_languages(save_multilanguages_value)
3422 save_multilanguages_value = retval[1]
3427 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3428 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3430 indexlinkentry.text = indexlink
3432 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)
3433 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3434 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3436 madewithentry.text = madewith
3438 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)
3442 recreate_theme_config = proc {
3443 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3445 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3447 $images_size.each { |s|
3448 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3450 if limit_sizes.include?(s['name'])
3458 tooltips.set_tip(cb, utf8(s['description']), nil)
3459 theme_sizes << { :widget => cb, :value => s['name'] }
3461 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3462 tooltips = Gtk::Tooltips.new
3463 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3464 if limit_sizes && limit_sizes.include?('original')
3467 theme_sizes << { :widget => cb, :value => 'original' }
3470 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3473 $allowed_N_values.each { |n|
3475 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3477 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3479 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3480 nperrowradios.add(Gtk::Label.new(' '))
3481 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3484 nperrows << { :widget => rb, :value => n.to_s }
3486 nperrowradios.show_all
3488 recreate_theme_config.call
3490 theme_button.signal_connect('clicked') {
3491 if newtheme = theme_choose(theme_button.label)
3494 theme_button.label = newtheme
3495 recreate_theme_config.call
3499 dialog.vbox.add(frame1)
3500 dialog.vbox.add(frame2)
3506 dialog.run { |response|
3507 if response == Gtk::Dialog::RESPONSE_OK
3508 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }