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 = create_window.set_title(filename)
271 msg 3, "filename: #{filename}"
272 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
273 #- typically this file won't exist in case of videos; try with the largest thumbnail around
274 if !File.exists?(dest_img)
275 if entry2type(filename) == 'video'
276 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
277 if not alternatives.empty?
278 dest_img = alternatives[-1]
281 push_mousecursor_wait
282 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
284 if !File.exists?(dest_img)
285 msg 2, _("Could not generate fullscreen thumbnail!")
290 evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(Gtk::Image.new(dest_img)).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
291 evt.signal_connect('button-press-event') { |this, event|
292 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
293 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
295 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
297 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
298 delete_item.signal_connect('activate') {
300 closures[:delete].call(false)
303 menu.popup(nil, nil, event.button, event.time)
306 evt.signal_connect('button-release-event') { |this, event|
308 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
309 msg 3, "gesture delete: click-drag right button to the bottom"
311 closures[:delete].call(false)
312 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
316 tooltips = Gtk::Tooltips.new
317 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
319 w.signal_connect('key-press-event') { |w,event|
320 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
322 closures[:delete].call(false)
326 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
327 b.signal_connect('clicked') { w.destroy }
330 vb.pack_start(evt, false, false)
331 vb.pack_end(bottom, false, false)
334 w.signal_connect('delete-event') { w.destroy }
335 w.window_position = Gtk::Window::POS_CENTER
339 def scroll_upper(scrolledwindow, ypos_top)
340 newval = scrolledwindow.vadjustment.value -
341 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
342 if newval < scrolledwindow.vadjustment.lower
343 newval = scrolledwindow.vadjustment.lower
345 scrolledwindow.vadjustment.value = newval
348 def scroll_lower(scrolledwindow, ypos_bottom)
349 newval = scrolledwindow.vadjustment.value +
350 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
351 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
352 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
354 scrolledwindow.vadjustment.value = newval
357 def autoscroll_if_needed(scrolledwindow, image, textview)
358 #- autoscroll if cursor or image is not visible, if possible
359 if image && image.window || textview.window
360 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
361 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
362 current_miny_visible = scrolledwindow.vadjustment.value
363 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
364 if ypos_top < current_miny_visible
365 scroll_upper(scrolledwindow, ypos_top)
366 elsif ypos_bottom > current_maxy_visible
367 scroll_lower(scrolledwindow, ypos_bottom)
372 def create_editzone(scrolledwindow, pagenum, image)
373 frame = Gtk::Frame.new
374 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
375 frame.set_shadow_type(Gtk::SHADOW_IN)
376 textview.signal_connect('key-press-event') { |w, event|
377 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
378 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
379 scrolledwindow.signal_emit('key-press-event', event)
381 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
382 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
383 if event.keyval == Gdk::Keyval::GDK_Up
384 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
385 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
387 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
390 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
391 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
393 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
400 candidate_undo_text = nil
401 textview.signal_connect('focus-in-event') { |w, event|
402 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
403 candidate_undo_text = textview.buffer.text
407 textview.signal_connect('key-release-event') { |w, event|
408 if candidate_undo_text && candidate_undo_text != textview.buffer.text
410 save_undo(_("text edit"),
412 save_text = textview.buffer.text
413 textview.buffer.text = text
415 $notebook.set_page(pagenum)
417 textview.buffer.text = save_text
419 $notebook.set_page(pagenum)
421 }, candidate_undo_text)
422 candidate_undo_text = nil
425 if event.state != 0 || ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
426 autoscroll_if_needed(scrolledwindow, image, textview)
431 return [ frame, textview ]
434 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
436 if !$modified_pixbufs[thumbnail_img]
437 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
438 elsif !$modified_pixbufs[thumbnail_img][:orig]
439 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
442 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
445 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
446 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
447 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
448 if pixbuf.height > desired_y
449 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
450 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
451 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
456 if $modified_pixbufs[thumbnail_img][:whitebalance]
457 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
460 #- fix gamma correction
461 if $modified_pixbufs[thumbnail_img][:gammacorrect]
462 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
465 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
468 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
471 #- update rotate attribute
472 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
473 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
475 if $config['rotate-set-exif'] == 'true'
476 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
479 $modified_pixbufs[thumbnail_img] ||= {}
480 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
481 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
483 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
486 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
489 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
491 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
493 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
494 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
496 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
497 $notebook.set_page(0)
498 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
503 def color_swap(xmldir, attributes_prefix)
505 if xmldir.attributes["#{attributes_prefix}color-swap"]
506 xmldir.delete_attribute("#{attributes_prefix}color-swap")
508 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
512 def enhance(xmldir, attributes_prefix)
514 if xmldir.attributes["#{attributes_prefix}enhance"]
515 xmldir.delete_attribute("#{attributes_prefix}enhance")
517 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
521 def change_seektime(xmldir, attributes_prefix, value)
523 xmldir.add_attribute("#{attributes_prefix}seektime", value)
526 def ask_new_seektime(xmldir, attributes_prefix)
528 value = xmldir.attributes["#{attributes_prefix}seektime"]
533 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
535 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
536 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
537 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
541 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
545 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
546 entry.signal_connect('key-press-event') { |w, event|
547 if event.keyval == Gdk::Keyval::GDK_Return
548 dialog.response(Gtk::Dialog::RESPONSE_OK)
550 elsif event.keyval == Gdk::Keyval::GDK_Escape
551 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
554 false #- propagate if needed
558 dialog.window_position = Gtk::Window::POS_MOUSE
561 dialog.run { |response|
564 if response == Gtk::Dialog::RESPONSE_OK
566 msg 3, "changing seektime to #{newval}"
567 return { :old => value, :new => newval }
574 def change_pano_amount(xmldir, attributes_prefix, value)
577 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
579 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
583 def ask_new_pano_amount(xmldir, attributes_prefix)
585 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
590 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
592 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
593 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
594 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
598 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
599 of this panorama image compared to other regular images. For example, if the panorama
600 was taken out of four photos on one row, counting the necessary overlap, the width of
601 this panorama image should probably be roughly three times the width of regular images.
603 With this information, booh will be able to generate panorama thumbnails looking
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 set_mainwindow_title(progress)
1952 filename = $orig_filename || $filename
1955 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
1957 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
1961 $main_window.title = 'booh - ' + File.basename(filename)
1963 $main_window.title = 'booh'
1968 def backend_wait_message(parent, msg, infopipe_path, mode)
1970 w.set_transient_for(parent)
1973 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1974 vb.pack_start(Gtk::Label.new(msg), false, false)
1976 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1977 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1978 if mode != 'one dir scan'
1979 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1981 if mode == 'web-album'
1982 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1983 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1985 vb.pack_start(Gtk::HSeparator.new, false, false)
1987 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1988 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1989 vb.pack_end(bottom, false, false)
1992 update_progression_title_pb1 = proc {
1993 if mode != 'web-album'
1994 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
1996 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2000 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2001 refresh_thread = Thread.new {
2002 directories_counter = 0
2003 while line = infopipe.gets
2004 if line =~ /^directories: (\d+), sizes: (\d+)/
2005 directories = $1.to_f + 1
2007 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2008 elements = $3.to_f + 1
2009 if mode == 'web-album'
2013 gtk_thread_protect { pb1_1.fraction = 0 }
2014 if mode != 'one dir scan'
2015 newtext = utf8(full_src_dir_to_rel($1, $2))
2016 newtext = '/' if newtext == ''
2017 gtk_thread_protect { pb1_2.text = newtext }
2018 directories_counter += 1
2019 gtk_thread_protect {
2020 pb1_2.fraction = directories_counter / directories
2021 update_progression_title_pb1.call
2024 elsif line =~ /^processing element$/
2025 element_counter += 1
2026 gtk_thread_protect {
2027 pb1_1.fraction = element_counter / elements
2028 update_progression_title_pb1.call
2030 elsif line =~ /^processing size$/
2031 element_counter += 1
2032 gtk_thread_protect {
2033 pb1_1.fraction = element_counter / elements
2034 update_progression_title_pb1.call
2036 elsif line =~ /^finished processing sizes$/
2037 gtk_thread_protect { pb1_1.fraction = 1 }
2038 elsif line =~ /^creating index.html$/
2039 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2040 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2041 directories_counter = 0
2042 elsif line =~ /^index.html: (.+)\|(.+)/
2043 newtext = utf8(full_src_dir_to_rel($1, $2))
2044 newtext = '/' if newtext == ''
2045 gtk_thread_protect { pb2.text = newtext }
2046 directories_counter += 1
2047 gtk_thread_protect {
2048 pb2.fraction = directories_counter / directories
2049 set_mainwindow_title(0.9 + pb2.fraction / 10)
2051 elsif line =~ /^die: (.*)$/
2058 w.signal_connect('delete-event') { w.destroy }
2059 w.signal_connect('destroy') {
2060 Thread.kill(refresh_thread)
2061 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2064 File.delete(infopipe_path)
2066 set_mainwindow_title(nil)
2068 w.window_position = Gtk::Window::POS_CENTER
2074 def call_backend(cmd, waitmsg, mode, params)
2075 pipe = Tempfile.new("boohpipe")
2077 system("mkfifo #{pipe.path}")
2078 cmd += " --info-pipe #{pipe.path}"
2079 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2084 id, exitstatus = Process.waitpid2(pid)
2085 gtk_thread_protect { w8.destroy }
2087 if params[:successmsg]
2088 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2090 if params[:closure_after]
2091 gtk_thread_protect(¶ms[:closure_after])
2093 elsif exitstatus == 15
2094 #- say nothing, user aborted
2096 gtk_thread_protect { show_popup($main_window,
2097 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2103 button.signal_connect('clicked') {
2104 Process.kill('SIGTERM', pid)
2108 def save_changes(*forced)
2109 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2113 $xmldir.delete_attribute('already-generated')
2115 propagate_children = proc { |xmldir|
2116 if xmldir.attributes['subdirs-caption']
2117 xmldir.delete_attribute('already-generated')
2119 xmldir.elements.each('dir') { |element|
2120 propagate_children.call(element)
2124 if $xmldir.child_byname_notattr('dir', 'deleted')
2125 new_title = $subalbums_title.buffer.text
2126 if new_title != $xmldir.attributes['subdirs-caption']
2127 parent = $xmldir.parent
2128 if parent.name == 'dir'
2129 parent.delete_attribute('already-generated')
2131 propagate_children.call($xmldir)
2133 $xmldir.add_attribute('subdirs-caption', new_title)
2134 $xmldir.elements.each('dir') { |element|
2135 if !element.attributes['deleted']
2136 path = element.attributes['path']
2137 newtext = $subalbums_edits[path][:editzone].buffer.text
2138 if element.attributes['subdirs-caption']
2139 if element.attributes['subdirs-caption'] != newtext
2140 propagate_children.call(element)
2142 element.add_attribute('subdirs-caption', newtext)
2143 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2145 if element.attributes['thumbnails-caption'] != newtext
2146 element.delete_attribute('already-generated')
2148 element.add_attribute('thumbnails-caption', newtext)
2149 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2155 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2156 if $xmldir.attributes['thumbnails-caption']
2157 path = $xmldir.attributes['path']
2158 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2160 elsif $xmldir.attributes['thumbnails-caption']
2161 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2164 if $xmldir.attributes['thumbnails-caption']
2165 if edit = $subalbums_edits[$xmldir.attributes['path']]
2166 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2170 #- remove and reinsert elements to reflect new ordering
2173 $xmldir.elements.each { |element|
2174 if element.name == 'image' || element.name == 'video'
2175 saves[element.attributes['filename']] = element.remove
2179 $autotable.current_order.each { |path|
2180 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2181 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2184 saves.each_key { |path|
2185 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2186 chld.add_attribute('deleted', 'true')
2190 def sort_by_exif_date
2194 $xmldir.elements.each { |element|
2195 if element.name == 'image' || element.name == 'video'
2196 current_order << element.attributes['filename']
2200 #- look for EXIF dates
2203 if current_order.size > 20
2205 w.set_transient_for($main_window)
2207 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2208 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2209 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2210 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2211 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2212 vb.pack_end(bottom, false, false)
2214 w.signal_connect('delete-event') { w.destroy }
2215 w.window_position = Gtk::Window::POS_CENTER
2219 b.signal_connect('clicked') { aborted = true }
2221 current_order.each { |f|
2223 if entry2type(f) == 'image'
2225 pb.fraction = i.to_f / current_order.size
2226 Gtk.main_iteration while Gtk.events_pending?
2227 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2229 dates[f] = date_time
2242 current_order.each { |f|
2243 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2245 dates[f] = date_time
2251 $xmldir.elements.each { |element|
2252 if element.name == 'image' || element.name == 'video'
2253 saves[element.attributes['filename']] = element.remove
2257 neworder = smartsort(current_order, dates)
2260 $xmldir.add_element(saves[f].name, saves[f].attributes)
2263 #- let the auto-table reflect new ordering
2267 def remove_all_captions
2270 $autotable.current_order.each { |path|
2271 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2272 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2274 save_undo(_("remove all captions"),
2276 texts.each_key { |key|
2277 $name2widgets[key][:textview].buffer.text = texts[key]
2279 $notebook.set_page(1)
2281 texts.each_key { |key|
2282 $name2widgets[key][:textview].buffer.text = ''
2284 $notebook.set_page(1)
2290 $selected_elements.each_key { |path|
2291 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2297 $selected_elements = {}
2301 $undo_tb.sensitive = $undo_mb.sensitive = false
2302 $redo_tb.sensitive = $redo_mb.sensitive = false
2308 $subalbums_vb.children.each { |chld|
2309 $subalbums_vb.remove(chld)
2311 $subalbums = Gtk::Table.new(0, 0, true)
2312 current_y_sub_albums = 0
2314 $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"])
2315 $subalbums_edits = {}
2316 subalbums_counter = 0
2317 subalbums_edits_bypos = {}
2319 add_subalbum = proc { |xmldir, counter|
2320 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2321 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2322 if xmldir == $xmldir
2323 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2324 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2325 caption = xmldir.attributes['thumbnails-caption']
2326 infotype = 'thumbnails'
2328 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2329 captionfile, caption = find_subalbum_caption_info(xmldir)
2330 infotype = find_subalbum_info_type(xmldir)
2332 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2333 hbox = Gtk::HBox.new
2334 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2336 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2339 my_gen_real_thumbnail = proc {
2340 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2343 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2344 f.add(img = Gtk::Image.new)
2345 my_gen_real_thumbnail.call
2347 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2349 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2350 $subalbums.attach(hbox,
2351 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2353 frame, textview = create_editzone($subalbums_sw, 0, img)
2354 textview.buffer.text = caption
2355 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2356 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2358 change_image = proc {
2359 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2361 Gtk::FileChooser::ACTION_OPEN,
2363 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2364 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2365 fc.transient_for = $main_window
2366 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))
2367 f.add(preview_img = Gtk::Image.new)
2369 fc.signal_connect('update-preview') { |w|
2371 if fc.preview_filename
2372 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2373 fc.preview_widget_active = true
2375 rescue Gdk::PixbufError
2376 fc.preview_widget_active = false
2379 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2381 old_file = captionfile
2382 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2383 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2384 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2385 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2387 new_file = fc.filename
2388 msg 3, "new captionfile is: #{fc.filename}"
2389 perform_changefile = proc {
2390 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2391 $modified_pixbufs.delete(thumbnail_file)
2392 xmldir.delete_attribute("#{infotype}-rotate")
2393 xmldir.delete_attribute("#{infotype}-color-swap")
2394 xmldir.delete_attribute("#{infotype}-enhance")
2395 xmldir.delete_attribute("#{infotype}-seektime")
2396 my_gen_real_thumbnail.call
2398 perform_changefile.call
2400 save_undo(_("change caption file for sub-album"),
2402 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2403 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2404 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2405 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2406 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2407 my_gen_real_thumbnail.call
2408 $notebook.set_page(0)
2410 perform_changefile.call
2411 $notebook.set_page(0)
2419 if File.exists?(thumbnail_file)
2420 File.delete(thumbnail_file)
2422 my_gen_real_thumbnail.call
2425 rotate_and_cleanup = proc { |angle|
2426 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2427 if File.exists?(thumbnail_file)
2428 File.delete(thumbnail_file)
2432 move = proc { |direction|
2435 save_changes('forced')
2436 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2437 if direction == 'up'
2438 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2439 subalbums_edits_bypos[oldpos - 1][:position] += 1
2441 if direction == 'down'
2442 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2443 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2445 if direction == 'top'
2446 for i in 1 .. oldpos - 1
2447 subalbums_edits_bypos[i][:position] += 1
2449 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2451 if direction == 'bottom'
2452 for i in oldpos + 1 .. subalbums_counter
2453 subalbums_edits_bypos[i][:position] -= 1
2455 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2459 $xmldir.elements.each('dir') { |element|
2460 if (!element.attributes['deleted'])
2461 elems << [ element.attributes['path'], element.remove ]
2464 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2465 each { |e| $xmldir.add_element(e[1]) }
2466 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2467 $xmldir.elements.each('descendant::dir') { |elem|
2468 elem.delete_attribute('already-generated')
2471 sel = $albums_tv.selection.selected_rows
2473 populate_subalbums_treeview(false)
2474 $albums_tv.selection.select_path(sel[0])
2477 color_swap_and_cleanup = proc {
2478 perform_color_swap_and_cleanup = proc {
2479 color_swap(xmldir, "#{infotype}-")
2480 my_gen_real_thumbnail.call
2482 perform_color_swap_and_cleanup.call
2484 save_undo(_("color swap"),
2486 perform_color_swap_and_cleanup.call
2487 $notebook.set_page(0)
2489 perform_color_swap_and_cleanup.call
2490 $notebook.set_page(0)
2495 change_seektime_and_cleanup = proc {
2496 if values = ask_new_seektime(xmldir, "#{infotype}-")
2497 perform_change_seektime_and_cleanup = proc { |val|
2498 change_seektime(xmldir, "#{infotype}-", val)
2499 my_gen_real_thumbnail.call
2501 perform_change_seektime_and_cleanup.call(values[:new])
2503 save_undo(_("specify seektime"),
2505 perform_change_seektime_and_cleanup.call(values[:old])
2506 $notebook.set_page(0)
2508 perform_change_seektime_and_cleanup.call(values[:new])
2509 $notebook.set_page(0)
2515 whitebalance_and_cleanup = proc {
2516 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2517 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2518 perform_change_whitebalance_and_cleanup = proc { |val|
2519 change_whitebalance(xmldir, "#{infotype}-", val)
2520 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2521 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2522 if File.exists?(thumbnail_file)
2523 File.delete(thumbnail_file)
2526 perform_change_whitebalance_and_cleanup.call(values[:new])
2528 save_undo(_("fix white balance"),
2530 perform_change_whitebalance_and_cleanup.call(values[:old])
2531 $notebook.set_page(0)
2533 perform_change_whitebalance_and_cleanup.call(values[:new])
2534 $notebook.set_page(0)
2540 gammacorrect_and_cleanup = proc {
2541 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2542 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2543 perform_change_gammacorrect_and_cleanup = proc { |val|
2544 change_gammacorrect(xmldir, "#{infotype}-", val)
2545 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2546 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2547 if File.exists?(thumbnail_file)
2548 File.delete(thumbnail_file)
2551 perform_change_gammacorrect_and_cleanup.call(values[:new])
2553 save_undo(_("gamma correction"),
2555 perform_change_gammacorrect_and_cleanup.call(values[:old])
2556 $notebook.set_page(0)
2558 perform_change_gammacorrect_and_cleanup.call(values[:new])
2559 $notebook.set_page(0)
2565 enhance_and_cleanup = proc {
2566 perform_enhance_and_cleanup = proc {
2567 enhance(xmldir, "#{infotype}-")
2568 my_gen_real_thumbnail.call
2571 perform_enhance_and_cleanup.call
2573 save_undo(_("enhance"),
2575 perform_enhance_and_cleanup.call
2576 $notebook.set_page(0)
2578 perform_enhance_and_cleanup.call
2579 $notebook.set_page(0)
2584 evtbox.signal_connect('button-press-event') { |w, event|
2585 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2587 rotate_and_cleanup.call(90)
2589 rotate_and_cleanup.call(-90)
2590 elsif $enhance.active?
2591 enhance_and_cleanup.call
2594 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2595 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2596 { :forbid_left => true, :forbid_right => true,
2597 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2598 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2599 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2600 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2601 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2603 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2608 evtbox.signal_connect('button-press-event') { |w, event|
2609 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2613 evtbox.signal_connect('button-release-event') { |w, event|
2614 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2615 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2616 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2617 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2618 msg 3, "gesture rotate: #{angle}"
2619 rotate_and_cleanup.call(angle)
2622 $gesture_press = nil
2625 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2626 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2627 current_y_sub_albums += 1
2630 if $xmldir.child_byname_notattr('dir', 'deleted')
2632 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2633 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2634 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2635 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2636 #- this album image/caption
2637 if $xmldir.attributes['thumbnails-caption']
2638 add_subalbum.call($xmldir, 0)
2641 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2642 $xmldir.elements.each { |element|
2643 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2644 #- element (image or video) of this album
2645 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2646 msg 3, "dest_img: #{dest_img}"
2647 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2648 total[element.name] += 1
2650 if element.name == 'dir' && !element.attributes['deleted']
2651 #- sub-album image/caption
2652 add_subalbum.call(element, subalbums_counter += 1)
2653 total[element.name] += 1
2656 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2657 total['image'], total['video'], total['dir'] ]))
2658 $subalbums_vb.add($subalbums)
2659 $subalbums_vb.show_all
2661 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2662 $notebook.get_tab_label($autotable_sw).sensitive = false
2663 $notebook.set_page(0)
2664 $thumbnails_title.buffer.text = ''
2666 $notebook.get_tab_label($autotable_sw).sensitive = true
2667 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2670 if !$xmldir.child_byname_notattr('dir', 'deleted')
2671 $notebook.get_tab_label($subalbums_sw).sensitive = false
2672 $notebook.set_page(1)
2674 $notebook.get_tab_label($subalbums_sw).sensitive = true
2678 def pixbuf_or_nil(filename)
2680 return Gdk::Pixbuf.new(filename)
2686 def theme_choose(current)
2687 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2689 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2690 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2691 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2693 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2694 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2695 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2696 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2697 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2698 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2699 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2700 treeview.signal_connect('button-press-event') { |w, event|
2701 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2702 dialog.response(Gtk::Dialog::RESPONSE_OK)
2706 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2708 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2711 iter[0] = File.basename(dir)
2712 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2713 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2714 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2715 if File.basename(dir) == current
2716 treeview.selection.select_iter(iter)
2720 dialog.set_default_size(700, 400)
2721 dialog.vbox.show_all
2722 dialog.run { |response|
2723 iter = treeview.selection.selected
2725 if response == Gtk::Dialog::RESPONSE_OK && iter
2726 return model.get_value(iter, 0)
2732 def show_password_protections
2733 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2734 child_iter = $albums_iters[xmldir.attributes['path']]
2735 if xmldir.attributes['password-protect']
2736 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2737 already_protected = true
2738 elsif already_protected
2739 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2741 pix = pix.saturate_and_pixelate(1, true)
2747 xmldir.elements.each('dir') { |elem|
2748 if !elem.attributes['deleted']
2749 examine_dir_elem.call(child_iter, elem, already_protected)
2753 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2756 def populate_subalbums_treeview(select_first)
2760 $subalbums_vb.children.each { |chld|
2761 $subalbums_vb.remove(chld)
2764 source = $xmldoc.root.attributes['source']
2765 msg 3, "source: #{source}"
2767 xmldir = $xmldoc.elements['//dir']
2768 if !xmldir || xmldir.attributes['path'] != source
2769 msg 1, _("Corrupted booh file...")
2773 append_dir_elem = proc { |parent_iter, xmldir|
2774 child_iter = $albums_ts.append(parent_iter)
2775 child_iter[0] = File.basename(xmldir.attributes['path'])
2776 child_iter[1] = xmldir.attributes['path']
2777 $albums_iters[xmldir.attributes['path']] = child_iter
2778 msg 3, "puttin location: #{xmldir.attributes['path']}"
2779 xmldir.elements.each('dir') { |elem|
2780 if !elem.attributes['deleted']
2781 append_dir_elem.call(child_iter, elem)
2785 append_dir_elem.call(nil, xmldir)
2786 show_password_protections
2788 $albums_tv.expand_all
2790 $albums_tv.selection.select_iter($albums_ts.iter_first)
2794 def select_current_theme
2795 select_theme($xmldoc.root.attributes['theme'],
2796 $xmldoc.root.attributes['limit-sizes'],
2797 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2798 $xmldoc.root.attributes['thumbnails-per-row'])
2801 def open_file(filename)
2805 $current_path = nil #- invalidate
2806 $modified_pixbufs = {}
2809 $subalbums_vb.children.each { |chld|
2810 $subalbums_vb.remove(chld)
2813 if !File.exists?(filename)
2814 return utf8(_("File not found."))
2818 $xmldoc = REXML::Document.new File.new(filename)
2823 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2824 if entry2type(filename).nil?
2825 return utf8(_("Not a booh file!"))
2827 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."))
2831 if !source = $xmldoc.root.attributes['source']
2832 return utf8(_("Corrupted booh file..."))
2835 if !dest = $xmldoc.root.attributes['destination']
2836 return utf8(_("Corrupted booh file..."))
2839 if !theme = $xmldoc.root.attributes['theme']
2840 return utf8(_("Corrupted booh file..."))
2843 if $xmldoc.root.attributes['version'] < '0.8.99.2'
2844 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2845 mark_document_as_dirty
2846 if $xmldoc.root.attributes['version'] < '0.8.4'
2847 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2848 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2849 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2850 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2851 if old_dest_dir != new_dest_dir
2852 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2854 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2855 xmldir.elements.each { |element|
2856 if %w(image video).include?(element.name) && !element.attributes['deleted']
2857 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2858 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2859 Dir[old_name + '*'].each { |file|
2860 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2861 file != new_file and sys("mv '#{file}' '#{new_file}'")
2864 if element.name == 'dir' && !element.attributes['deleted']
2865 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2866 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2867 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2871 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2875 $xmldoc.root.add_attribute('version', $VERSION)
2878 select_current_theme
2880 $filename = filename
2881 set_mainwindow_title(nil)
2882 $default_size['thumbnails'] =~ /(.*)x(.*)/
2883 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2884 $albums_thumbnail_size =~ /(.*)x(.*)/
2885 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2887 populate_subalbums_treeview(true)
2889 $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
2893 def open_file_user(filename)
2894 result = open_file(filename)
2896 $config['last-opens'] ||= []
2897 if $config['last-opens'][-1] != utf8(filename)
2898 $config['last-opens'] << utf8(filename)
2900 $orig_filename = $filename
2901 tmp = Tempfile.new("boohtemp")
2904 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2906 $tempfiles << $filename << "#{$filename}.backup"
2908 $orig_filename = nil
2914 if !ask_save_modifications(utf8(_("Save this album?")),
2915 utf8(_("Do you want to save the changes to this album?")),
2916 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2919 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2921 Gtk::FileChooser::ACTION_OPEN,
2923 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2924 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2925 fc.set_current_folder(File.expand_path("~/.booh"))
2926 fc.transient_for = $main_window
2929 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2930 push_mousecursor_wait(fc)
2931 msg = open_file_user(fc.filename)
2946 def additional_booh_options
2949 options += "--mproc #{$config['mproc'].to_i} "
2951 options += "--comments-format '#{$config['comments-format']}' "
2952 if $config['transcode-videos']
2953 options += "--transcode-videos '#{$config['transcode-videos']}' "
2958 def ask_multi_languages(value)
2960 spl = value.split(',')
2961 value = [ spl[0..-2], spl[-1] ]
2964 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
2967 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2968 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2970 lbl = Gtk::Label.new
2972 _("You can choose to activate <b>multi-languages</b> support for this web-album
2973 (it will work only if you publish your web-album on an Apache web-server). This will
2974 use the MultiViews feature of Apache; the pages will be served according to the
2975 value of the Accept-Language HTTP header sent by the web browsers, so that people
2976 with different languages preferences will be able to browse your web-album with
2977 navigation in their language (if language is available).
2980 dialog.vbox.add(lbl)
2981 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
2982 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
2983 add(languages = Gtk::Button.new))))
2985 pick_languages = proc {
2986 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
2989 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2990 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2992 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
2993 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
2995 SUPPORTED_LANGUAGES.each { |lang|
2996 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
2997 if ! value.nil? && value[0].include?(lang)
3003 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3004 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3005 fallback_language = nil
3006 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3007 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3008 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3009 fbl_rb.active = true
3010 fallback_language = SUPPORTED_LANGUAGES[0]
3012 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3013 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3014 rb.signal_connect('clicked') { fallback_language = lang }
3015 if ! value.nil? && value[1] == lang
3020 dialog2.window_position = Gtk::Window::POS_MOUSE
3024 dialog2.run { |response|
3026 if resp == Gtk::Dialog::RESPONSE_OK
3028 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3029 value[1] = fallback_language
3030 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| utf8(langname(v)) }.join(', '), utf8(langname(value[1])) ])
3037 languages.signal_connect('clicked') {
3040 dialog.window_position = Gtk::Window::POS_MOUSE
3044 rb_yes.active = true
3045 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| utf8(langname(v)) }.join(', '), utf8(langname(value[1])) ])
3047 rb_no.signal_connect('clicked') {
3051 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3064 dialog.run { |response|
3069 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3071 return [ true, nil ]
3073 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3082 if !ask_save_modifications(utf8(_("Save this album?")),
3083 utf8(_("Do you want to save the changes to this album?")),
3084 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3087 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3089 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3090 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3091 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3093 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3094 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
3095 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3096 tbl.attach(src = Gtk::Entry.new,
3097 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3098 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3099 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3100 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
3101 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3102 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3103 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3104 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3105 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3106 tbl.attach(dest = Gtk::Entry.new,
3107 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3108 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3109 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3110 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3111 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3112 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3113 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3114 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3115 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3117 tooltips = Gtk::Tooltips.new
3118 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3119 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3120 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3121 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3122 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3123 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
3124 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)
3125 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3126 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3127 nperpage_model = Gtk::ListStore.new(String, String)
3128 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3129 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3130 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3131 nperpagecombo.set_attributes(crt, { :markup => 0 })
3132 iter = nperpage_model.append
3133 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3135 [ 12, 20, 30, 40, 50 ].each { |v|
3136 iter = nperpage_model.append
3137 iter[0] = iter[1] = v.to_s
3139 nperpagecombo.active = 0
3141 multilanguages_value = nil
3142 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new(utf8(_("Multi-languages: disabled."))), false, false, 0).
3143 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3144 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)
3145 multilanguages.signal_connect('clicked') {
3146 retval = ask_multi_languages(multilanguages_value)
3148 multilanguages_value = retval[1]
3150 if multilanguages_value
3151 ml_label.text = utf8(_("Multi-languages: enabled."))
3153 ml_label.text = utf8(_("Multi-languages: disabled."))
3157 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3158 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3159 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)
3160 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3161 pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
3162 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)
3164 src_nb_calculated_for = ''
3166 process_src_nb = proc {
3167 if src.text != src_nb_calculated_for
3168 src_nb_calculated_for = src.text
3170 Thread.kill(src_nb_thread)
3173 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3174 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3176 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3177 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3178 src_nb_thread = Thread.new {
3179 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3180 total = { 'image' => 0, 'video' => 0, nil => 0 }
3181 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3182 if File.basename(dir) =~ /^\./
3186 Dir.entries(dir.chomp).each { |file|
3187 total[entry2type(file)] += 1
3189 rescue Errno::EACCES, Errno::ENOENT
3193 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3197 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3200 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3206 timeout_src_nb = Gtk.timeout_add(100) {
3210 src_browse.signal_connect('clicked') {
3211 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
3213 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3215 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3216 fc.transient_for = $main_window
3217 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3218 src.text = utf8(fc.filename)
3220 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3225 dest_browse.signal_connect('clicked') {
3226 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3228 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3230 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3231 fc.transient_for = $main_window
3232 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3233 dest.text = utf8(fc.filename)
3238 conf_browse.signal_connect('clicked') {
3239 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3241 Gtk::FileChooser::ACTION_SAVE,
3243 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3244 fc.transient_for = $main_window
3245 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3246 fc.set_current_folder(File.expand_path("~/.booh"))
3247 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3248 conf.text = utf8(fc.filename)
3255 recreate_theme_config = proc {
3256 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3258 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3259 $images_size.each { |s|
3260 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3264 tooltips.set_tip(cb, utf8(s['description']), nil)
3265 theme_sizes << { :widget => cb, :value => s['name'] }
3267 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3268 tooltips = Gtk::Tooltips.new
3269 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3270 theme_sizes << { :widget => cb, :value => 'original' }
3273 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3276 $allowed_N_values.each { |n|
3278 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3280 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3282 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3286 nperrows << { :widget => rb, :value => n }
3288 nperrowradios.show_all
3290 recreate_theme_config.call
3292 theme_button.signal_connect('clicked') {
3293 if newtheme = theme_choose(theme_button.label)
3294 theme_button.label = newtheme
3295 recreate_theme_config.call
3299 dialog.vbox.add(frame1)
3300 dialog.vbox.add(frame2)
3306 dialog.run { |response|
3307 if response == Gtk::Dialog::RESPONSE_OK
3308 srcdir = from_utf8_safe(src.text)
3309 destdir = from_utf8_safe(dest.text)
3310 confpath = from_utf8_safe(conf.text)
3311 if src.text != '' && srcdir == ''
3312 show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3314 elsif !File.directory?(srcdir)
3315 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3317 elsif dest.text != '' && destdir == ''
3318 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3320 elsif destdir != make_dest_filename(destdir)
3321 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3323 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3324 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3325 inside it will be permanently removed before creating the web-album!
3326 Are you sure you want to continue?")), { :okcancel => true })
3328 elsif File.exists?(destdir) && !File.directory?(destdir)
3329 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3331 elsif conf.text == ''
3332 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3334 elsif conf.text != '' && confpath == ''
3335 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3337 elsif File.directory?(confpath)
3338 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3340 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3341 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3343 system("mkdir '#{destdir}'")
3344 if !File.directory?(destdir)
3345 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3357 srcdir = from_utf8(src.text)
3358 destdir = from_utf8(dest.text)
3359 configskel = File.expand_path(from_utf8(conf.text))
3360 theme = theme_button.label
3361 #- some sort of automatic theme preference
3362 $config['default-theme'] = theme
3363 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3364 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3365 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3366 opt432 = optimize432.active?
3367 madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3368 indexlink = indexlinkentry.text.gsub('\'', ''')
3371 Thread.kill(src_nb_thread)
3372 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3375 Gtk.timeout_remove(timeout_src_nb)
3378 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3379 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3380 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3381 (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
3382 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3383 utf8(_("Please wait while scanning source directory...")),
3385 { :closure_after => proc { open_file_user(configskel) } })
3390 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3392 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3393 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3394 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3396 source = $xmldoc.root.attributes['source']
3397 dest = $xmldoc.root.attributes['destination']
3398 theme = $xmldoc.root.attributes['theme']
3399 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3400 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3401 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3402 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3404 limit_sizes = limit_sizes.split(/,/)
3406 madewith = ($xmldoc.root.attributes['made-with'] || '').gsub(''', '\'')
3407 indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub(''', '\'')
3408 save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3410 tooltips = Gtk::Tooltips.new
3411 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3412 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3413 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3414 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3415 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3416 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3417 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3418 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3419 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3420 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3421 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3422 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3423 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3425 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3426 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3427 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3428 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3429 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3430 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3431 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)
3432 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3433 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3434 nperpage_model = Gtk::ListStore.new(String, String)
3435 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3436 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3437 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3438 nperpagecombo.set_attributes(crt, { :markup => 0 })
3439 iter = nperpage_model.append
3440 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3442 [ 12, 20, 30, 40, 50 ].each { |v|
3443 iter = nperpage_model.append
3444 iter[0] = iter[1] = v.to_s
3445 if nperpage && nperpage == v.to_s
3446 nperpagecombo.active_iter = iter
3449 if nperpagecombo.active_iter.nil?
3450 nperpagecombo.active = 0
3453 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3454 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3455 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)
3457 if save_multilanguages_value
3458 ml_label.text = utf8(_("Multi-languages: enabled."))
3460 ml_label.text = utf8(_("Multi-languages: disabled."))
3464 multilanguages.signal_connect('clicked') {
3465 retval = ask_multi_languages(save_multilanguages_value)
3467 save_multilanguages_value = retval[1]
3472 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3473 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3475 indexlinkentry.text = indexlink
3477 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)
3478 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3479 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3481 madewithentry.text = madewith
3483 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)
3487 recreate_theme_config = proc {
3488 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3490 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3492 $images_size.each { |s|
3493 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3495 if limit_sizes.include?(s['name'])
3503 tooltips.set_tip(cb, utf8(s['description']), nil)
3504 theme_sizes << { :widget => cb, :value => s['name'] }
3506 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3507 tooltips = Gtk::Tooltips.new
3508 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3509 if limit_sizes && limit_sizes.include?('original')