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-2006 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 'rexml/document'
37 require 'booh/booh-lib'
39 require 'booh/UndoHandler'
40 require 'booh/Synchronizator'
45 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
47 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
50 #- default values for some globals
54 $ignore_videos = false
55 $button1_pressed_autotable = false
56 $generated_outofline = false
59 puts _("Usage: %s [OPTION]...") % File.basename($0)
61 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
66 parser = GetoptLong.new
67 parser.set_options(*$options.collect { |ary| ary[0..2] })
69 parser.each_option do |name, arg|
75 when '--verbose-level'
76 $verbose_level = arg.to_i
89 $config_file = File.expand_path('~/.booh-gui-rc')
90 if File.readable?($config_file)
91 $xmldoc = REXML::Document.new(File.new($config_file))
92 $xmldoc.root.elements.each { |element|
93 txt = element.get_text
95 if txt.value =~ /~~~/ || element.name == 'last-opens'
96 $config[element.name] = txt.value.split(/~~~/)
98 $config[element.name] = txt.value
100 elsif element.elements.size == 0
101 $config[element.name] = ''
103 $config[element.name] = {}
104 element.each { |chld|
106 $config[element.name][chld.name] = txt ? txt.value : nil
111 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
112 $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
113 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
114 $config['comments-format'] ||= '%t'
115 if !FileTest.directory?(File.expand_path('~/.booh'))
116 system("mkdir ~/.booh")
118 if $config['mproc'].nil?
120 for line in IO.readlines('/proc/cpuinfo') do
121 line =~ /^processor/ and cpus += 1
124 $config['mproc'] = cpus
127 $config['rotate-set-exif'] ||= 'true'
133 if !system("which convert >/dev/null 2>/dev/null")
134 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
135 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
138 if !system("which identify >/dev/null 2>/dev/null")
139 show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
140 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
142 if !system("which exif >/dev/null 2>/dev/null")
143 show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
145 missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
147 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
150 viewer_binary = $config['video-viewer'].split.first
151 if viewer_binary && !File.executable?(viewer_binary)
152 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
153 You should fix this in Edit/Preferences so that you can view videos.
155 Problem was: '%s' is not an executable file.
156 Hint: don't forget to specify the full path to the executable,
157 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
159 image_editor_binary = $config['image-editor'].split.first
160 if image_editor_binary && !File.executable?(image_editor_binary)
161 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
162 You should fix this in Edit/Preferences so that you can edit images externally.
164 Problem was: '%s' is not an executable file.
165 Hint: don't forget to specify the full path to the executable,
166 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
168 browser_binary = $config['browser'].split.first
169 if browser_binary && !File.executable?(browser_binary)
170 show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
171 You should fix this in Edit/Preferences so that you can open URLs.
173 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
178 if $config['last-opens'] && $config['last-opens'].size > 10
179 $config['last-opens'] = $config['last-opens'][-10, 10]
182 ios = File.open($config_file, "w")
183 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
184 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
185 $config.each_pair { |key, value|
186 elem = $xmldoc.root.add_element key
188 $config[key].each_pair { |subkey, subvalue|
189 subelem = elem.add_element subkey
190 subelem.add_text subvalue.to_s
192 elsif value.is_a? Array
193 elem.add_text value.join('~~~')
198 elem.add_text value.to_s
202 $xmldoc.write(ios, 0)
205 $tempfiles.each { |f|
212 def set_mousecursor(what, *widget)
213 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
214 if widget[0] && widget[0].window
215 widget[0].window.cursor = cursor
217 if $main_window && $main_window.window
218 $main_window.window.cursor = cursor
220 $current_cursor = what
222 def set_mousecursor_wait(*widget)
223 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
224 if Thread.current == Thread.main
225 Gtk.main_iteration while Gtk.events_pending?
228 def set_mousecursor_normal(*widget)
229 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
231 def push_mousecursor_wait(*widget)
232 if $current_cursor != Gdk::Cursor::WATCH
233 $save_cursor = $current_cursor
234 gtk_thread_protect { set_mousecursor_wait(*widget) }
237 def pop_mousecursor(*widget)
238 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
242 source = $xmldoc.root.attributes['source']
243 dest = $xmldoc.root.attributes['destination']
244 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
247 def full_src_dir_to_rel(path, source)
248 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
251 def build_full_dest_filename(filename)
252 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
255 def save_undo(name, closure, *params)
256 UndoHandler.save_undo(name, closure, [ *params ])
257 $undo_tb.sensitive = $undo_mb.sensitive = true
258 $redo_tb.sensitive = $redo_mb.sensitive = false
261 def view_element(filename, closures)
262 if entry2type(filename) == 'video'
263 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
269 w = Gtk::Window.new.set_title(filename)
271 msg 3, "filename: #{filename}"
272 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
273 #- typically this file won't exist in case of videos; try with the largest thumbnail around
274 if !File.exists?(dest_img)
275 if entry2type(filename) == 'video'
276 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
277 if not alternatives.empty?
278 dest_img = alternatives[-1]
281 push_mousecursor_wait
282 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
284 if !File.exists?(dest_img)
285 msg 2, _("Could not generate fullscreen thumbnail!")
290 evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(Gtk::Image.new(dest_img)).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
291 evt.signal_connect('button-press-event') { |this, event|
292 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
293 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
295 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
297 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
298 delete_item.signal_connect('activate') {
300 closures[:delete].call(false)
303 menu.popup(nil, nil, event.button, event.time)
306 evt.signal_connect('button-release-event') { |this, event|
308 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
309 msg 3, "gesture delete: click-drag right button to the bottom"
311 closures[:delete].call(false)
312 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
316 tooltips = Gtk::Tooltips.new
317 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
319 w.signal_connect('key-press-event') { |w,event|
320 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
322 closures[:delete].call(false)
326 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
327 b.signal_connect('clicked') { w.destroy }
330 vb.pack_start(evt, false, false)
331 vb.pack_end(bottom, false, false)
334 w.signal_connect('delete-event') { w.destroy }
335 w.window_position = Gtk::Window::POS_CENTER
339 def scroll_upper(scrolledwindow, ypos_top)
340 newval = scrolledwindow.vadjustment.value -
341 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
342 if newval < scrolledwindow.vadjustment.lower
343 newval = scrolledwindow.vadjustment.lower
345 scrolledwindow.vadjustment.value = newval
348 def scroll_lower(scrolledwindow, ypos_bottom)
349 newval = scrolledwindow.vadjustment.value +
350 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
351 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
352 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
354 scrolledwindow.vadjustment.value = newval
357 def autoscroll_if_needed(scrolledwindow, image, textview)
358 #- autoscroll if cursor or image is not visible, if possible
359 if image && image.window || textview.window
360 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
361 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
362 current_miny_visible = scrolledwindow.vadjustment.value
363 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
364 if ypos_top < current_miny_visible
365 scroll_upper(scrolledwindow, ypos_top)
366 elsif ypos_bottom > current_maxy_visible
367 scroll_lower(scrolledwindow, ypos_bottom)
372 def create_editzone(scrolledwindow, pagenum, image)
373 frame = Gtk::Frame.new
374 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
375 frame.set_shadow_type(Gtk::SHADOW_IN)
376 textview.signal_connect('key-press-event') { |w, event|
377 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
378 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
379 scrolledwindow.signal_emit('key-press-event', event)
381 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
382 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
383 if event.keyval == Gdk::Keyval::GDK_Up
384 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
385 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
387 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
390 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
391 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
393 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
400 candidate_undo_text = nil
401 textview.signal_connect('focus-in-event') { |w, event|
402 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
403 candidate_undo_text = textview.buffer.text
407 textview.signal_connect('key-release-event') { |w, event|
408 if candidate_undo_text && candidate_undo_text != textview.buffer.text
410 save_undo(_("text edit"),
412 save_text = textview.buffer.text
413 textview.buffer.text = text
415 $notebook.set_page(pagenum)
417 textview.buffer.text = save_text
419 $notebook.set_page(pagenum)
421 }, candidate_undo_text)
422 candidate_undo_text = nil
425 if event.state != 0 || ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
426 autoscroll_if_needed(scrolledwindow, image, textview)
431 return [ frame, textview ]
434 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
436 if !$modified_pixbufs[thumbnail_img]
437 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
438 elsif !$modified_pixbufs[thumbnail_img][:orig]
439 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
442 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
445 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
446 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
447 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
448 if pixbuf.height > desired_y
449 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
450 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
451 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
456 if $modified_pixbufs[thumbnail_img][:whitebalance]
457 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
460 #- fix gamma correction
461 if $modified_pixbufs[thumbnail_img][:gammacorrect]
462 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
465 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
468 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
471 #- update rotate attribute
472 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
473 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
475 if $config['rotate-set-exif'] == 'true'
476 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
479 $modified_pixbufs[thumbnail_img] ||= {}
480 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
481 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
483 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
486 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
489 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
491 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
493 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
494 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
496 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
497 $notebook.set_page(0)
498 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
503 def color_swap(xmldir, attributes_prefix)
505 if xmldir.attributes["#{attributes_prefix}color-swap"]
506 xmldir.delete_attribute("#{attributes_prefix}color-swap")
508 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
512 def enhance(xmldir, attributes_prefix)
514 if xmldir.attributes["#{attributes_prefix}enhance"]
515 xmldir.delete_attribute("#{attributes_prefix}enhance")
517 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
521 def change_seektime(xmldir, attributes_prefix, value)
523 xmldir.add_attribute("#{attributes_prefix}seektime", value)
526 def ask_new_seektime(xmldir, attributes_prefix)
528 value = xmldir.attributes["#{attributes_prefix}seektime"]
533 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
535 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
536 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
537 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
541 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
545 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
546 entry.signal_connect('key-press-event') { |w, event|
547 if event.keyval == Gdk::Keyval::GDK_Return
548 dialog.response(Gtk::Dialog::RESPONSE_OK)
550 elsif event.keyval == Gdk::Keyval::GDK_Escape
551 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
554 false #- propagate if needed
558 dialog.window_position = Gtk::Window::POS_MOUSE
561 dialog.run { |response|
564 if response == Gtk::Dialog::RESPONSE_OK
566 msg 3, "changing seektime to #{newval}"
567 return { :old => value, :new => newval }
574 def change_pano_amount(xmldir, attributes_prefix, value)
577 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
579 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
583 def ask_new_pano_amount(xmldir, attributes_prefix)
585 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
590 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
592 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
593 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
594 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
598 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
599 of this panorama image compared to other regular images. For example, if the panorama
600 was taken out of four photos on one row, counting the necessary overlap, the width of
601 this panorama image should probably be roughly three times the width of regular images.
603 With this information, booh will be able to generate panorama thumbnails looking
607 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("none (not a panorama image)")))).
608 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
609 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
610 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
611 spin.signal_connect('value-changed') {
614 dialog.window_position = Gtk::Window::POS_MOUSE
617 spin.value = value.to_f
624 dialog.run { |response|
628 newval = spin.value.to_f
631 if response == Gtk::Dialog::RESPONSE_OK
633 msg 3, "changing panorama amount to #{newval}"
634 return { :old => value, :new => newval }
641 def change_whitebalance(xmlelem, attributes_prefix, value)
643 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
646 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
648 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
649 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
650 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
651 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
652 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
653 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
654 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
655 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
656 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
657 $modified_pixbufs[thumbnail_img] ||= {}
658 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
659 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
661 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
662 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
664 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
667 $modified_pixbufs[thumbnail_img] ||= {}
668 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
670 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
673 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
674 #- init $modified_pixbufs correctly
675 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
677 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
679 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
681 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
682 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
683 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
687 _("You can fix the <b>white balance</b> of the image, if your image is too blue
688 or too yellow because your camera didn't detect the light correctly. Drag the
689 slider below the image to the left for more blue, to the right for more yellow.
693 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
695 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
697 dialog.window_position = Gtk::Window::POS_MOUSE
701 timeout = Gtk.timeout_add(100) {
702 if hs.value != lastval
705 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
711 dialog.run { |response|
712 Gtk.timeout_remove(timeout)
713 if response == Gtk::Dialog::RESPONSE_OK
715 newval = hs.value.to_s
716 msg 3, "changing white balance to #{newval}"
718 return { :old => value, :new => newval }
721 $modified_pixbufs[thumbnail_img] ||= {}
722 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
723 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
731 def change_gammacorrect(xmlelem, attributes_prefix, value)
733 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
736 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
738 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
739 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
740 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
741 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
742 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
743 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
744 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
745 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
746 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
747 $modified_pixbufs[thumbnail_img] ||= {}
748 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
749 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
751 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
752 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
754 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
757 $modified_pixbufs[thumbnail_img] ||= {}
758 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
760 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
763 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
764 #- init $modified_pixbufs correctly
765 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
767 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
769 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
771 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
772 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
773 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
777 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
778 or too bright. Drag the slider below the image.
782 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
784 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
786 dialog.window_position = Gtk::Window::POS_MOUSE
790 timeout = Gtk.timeout_add(100) {
791 if hs.value != lastval
794 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
800 dialog.run { |response|
801 Gtk.timeout_remove(timeout)
802 if response == Gtk::Dialog::RESPONSE_OK
804 newval = hs.value.to_s
805 msg 3, "gamma correction to #{newval}"
807 return { :old => value, :new => newval }
810 $modified_pixbufs[thumbnail_img] ||= {}
811 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
812 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
820 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
821 File.delete(destfile)
822 #- type can be 'element' or 'subdir'
824 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
826 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
830 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
832 push_mousecursor_wait
833 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
836 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
842 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
843 distribute_multiple_call = Proc.new { |action, arg|
844 $selected_elements.each_key { |path|
845 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
847 if possible_actions[:can_multiple] && $selected_elements.length > 0
848 UndoHandler.begin_batch
849 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
850 UndoHandler.end_batch
852 closures[action].call(arg)
854 $selected_elements = {}
857 if optionals.include?('change_image')
858 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
859 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
860 changeimg.signal_connect('activate') { closures[:change].call }
861 menu.append(Gtk::SeparatorMenuItem.new)
863 if !possible_actions[:can_multiple] || $selected_elements.length == 0
866 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
867 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
868 view.signal_connect('activate') { closures[:view].call }
870 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
871 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
872 view.signal_connect('activate') { closures[:view].call }
873 menu.append(Gtk::SeparatorMenuItem.new)
876 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
877 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
878 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
879 exif.signal_connect('activate') { show_popup($main_window,
880 utf8(`exif -m '#{fullpath}'`),
881 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
882 menu.append(Gtk::SeparatorMenuItem.new)
885 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
886 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
887 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
888 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
889 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
890 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
891 if !possible_actions[:can_multiple] || $selected_elements.length == 0
892 menu.append(Gtk::SeparatorMenuItem.new)
893 if !possible_actions[:forbid_left]
894 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
895 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
896 moveleft.signal_connect('activate') { closures[:move].call('left') }
897 if !possible_actions[:can_left]
898 moveleft.sensitive = false
901 if !possible_actions[:forbid_right]
902 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
903 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
904 moveright.signal_connect('activate') { closures[:move].call('right') }
905 if !possible_actions[:can_right]
906 moveright.sensitive = false
909 if optionals.include?('move_top')
910 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
911 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
912 movetop.signal_connect('activate') { closures[:move].call('top') }
913 if !possible_actions[:can_top]
914 movetop.sensitive = false
917 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
918 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
919 moveup.signal_connect('activate') { closures[:move].call('up') }
920 if !possible_actions[:can_up]
921 moveup.sensitive = false
923 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
924 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
925 movedown.signal_connect('activate') { closures[:move].call('down') }
926 if !possible_actions[:can_down]
927 movedown.sensitive = false
929 if optionals.include?('move_bottom')
930 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
931 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
932 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
933 if !possible_actions[:can_bottom]
934 movebottom.sensitive = false
939 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
940 menu.append(Gtk::SeparatorMenuItem.new)
941 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
942 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
943 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
944 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
945 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
946 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
947 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
948 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
949 seektime.signal_connect('activate') {
950 if possible_actions[:can_multiple] && $selected_elements.length > 0
951 if values = ask_new_seektime(nil, '')
952 distribute_multiple_call.call(:seektime, values)
955 closures[:seektime].call
960 menu.append( Gtk::SeparatorMenuItem.new)
961 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
962 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
963 gammacorrect.signal_connect('activate') {
964 if possible_actions[:can_multiple] && $selected_elements.length > 0
965 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
966 distribute_multiple_call.call(:gammacorrect, values)
969 closures[:gammacorrect].call
972 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
973 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
974 whitebalance.signal_connect('activate') {
975 if possible_actions[:can_multiple] && $selected_elements.length > 0
976 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
977 distribute_multiple_call.call(:whitebalance, values)
980 closures[:whitebalance].call
983 if !possible_actions[:can_multiple] || $selected_elements.length == 0
984 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
985 _("Enhance constrast"))))
987 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
989 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
990 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
991 if type == 'image' && possible_actions[:can_panorama]
992 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
993 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
994 panorama.signal_connect('activate') {
995 if possible_actions[:can_multiple] && $selected_elements.length > 0
996 if values = ask_new_pano_amount(nil, '')
997 distribute_multiple_call.call(:pano, values)
1000 distribute_multiple_call.call(:pano)
1004 menu.append( Gtk::SeparatorMenuItem.new)
1005 if optionals.include?('delete')
1006 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1007 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1008 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1009 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1010 paste_item.signal_connect('activate') { closures[:paste].call }
1011 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1012 clear_item.signal_connect('activate') { $cuts = [] }
1014 paste_item.sensitive = clear_item.sensitive = false
1017 menu.append( Gtk::SeparatorMenuItem.new)
1019 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1020 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1021 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1022 editexternally.signal_connect('activate') {
1023 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1028 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1029 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1030 if optionals.include?('delete')
1031 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1032 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1035 menu.popup(nil, nil, event.button, event.time)
1038 def delete_current_subalbum
1040 sel = $albums_tv.selection.selected_rows
1041 $xmldir.elements.each { |e|
1042 if e.name == 'image' || e.name == 'video'
1043 e.add_attribute('deleted', 'true')
1046 #- branch if we have a non deleted subalbum
1047 if $xmldir.child_byname_notattr('dir', 'deleted')
1048 $xmldir.delete_attribute('thumbnails-caption')
1049 $xmldir.delete_attribute('thumbnails-captionfile')
1051 $xmldir.add_attribute('deleted', 'true')
1053 while moveup.parent.name == 'dir'
1054 moveup = moveup.parent
1055 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1056 moveup.add_attribute('deleted', 'true')
1063 save_changes('forced')
1064 populate_subalbums_treeview(false)
1065 $albums_tv.selection.select_path(sel[0])
1071 $current_path = nil #- prevent save_changes from being rerun again
1072 sel = $albums_tv.selection.selected_rows
1073 restore_one = proc { |xmldir|
1074 xmldir.elements.each { |e|
1075 if e.name == 'dir' && e.attributes['deleted']
1078 e.delete_attribute('deleted')
1081 restore_one.call($xmldir)
1082 populate_subalbums_treeview(false)
1083 $albums_tv.selection.select_path(sel[0])
1086 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1089 frame1 = Gtk::Frame.new
1090 fullpath = from_utf8("#{$current_path}/#{filename}")
1092 my_gen_real_thumbnail = proc {
1093 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1097 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1098 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1099 pack_start(img = Gtk::Image.new).
1100 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1101 px, mask = pxb.render_pixmap_and_mask(0)
1102 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1103 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1105 frame1.add(img = Gtk::Image.new)
1108 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1109 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1110 my_gen_real_thumbnail.call
1112 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1115 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1117 tooltips = Gtk::Tooltips.new
1118 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1119 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1121 frame2, textview = create_editzone($autotable_sw, 1, img)
1122 textview.buffer.text = caption
1123 textview.set_justification(Gtk::Justification::CENTER)
1125 vbox = Gtk::VBox.new(false, 5)
1126 vbox.pack_start(evtbox, false, false)
1127 vbox.pack_start(frame2, false, false)
1128 autotable.append(vbox, filename)
1130 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1131 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1133 #- to be able to find widgets by name
1134 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1136 cleanup_all_thumbnails = proc {
1137 #- remove out of sync images
1138 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1139 for sizeobj in $images_size
1140 File.delete("#{dest_img_base}-#{sizeobj['fullscreen']}.jpg", "#{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
1146 cleanup_all_thumbnails.call
1147 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1149 $xmldir.delete_attribute('already-generated')
1150 my_gen_real_thumbnail.call
1153 rotate_and_cleanup = proc { |angle|
1154 cleanup_all_thumbnails.call
1155 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1158 move = proc { |direction|
1159 do_method = "move_#{direction}"
1160 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1162 done = autotable.method(do_method).call(vbox)
1163 textview.grab_focus #- because if moving, focus is stolen
1167 save_undo(_("move %s") % direction,
1169 autotable.method(undo_method).call(vbox)
1170 textview.grab_focus #- because if moving, focus is stolen
1171 autoscroll_if_needed($autotable_sw, img, textview)
1172 $notebook.set_page(1)
1174 autotable.method(do_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)
1183 color_swap_and_cleanup = proc {
1184 perform_color_swap_and_cleanup = proc {
1185 cleanup_all_thumbnails.call
1186 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1187 my_gen_real_thumbnail.call
1190 perform_color_swap_and_cleanup.call
1192 save_undo(_("color swap"),
1194 perform_color_swap_and_cleanup.call
1196 autoscroll_if_needed($autotable_sw, img, textview)
1197 $notebook.set_page(1)
1199 perform_color_swap_and_cleanup.call
1201 autoscroll_if_needed($autotable_sw, img, textview)
1202 $notebook.set_page(1)
1207 change_seektime_and_cleanup_real = proc { |values|
1208 perform_change_seektime_and_cleanup = proc { |val|
1209 cleanup_all_thumbnails.call
1210 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1211 my_gen_real_thumbnail.call
1213 perform_change_seektime_and_cleanup.call(values[:new])
1215 save_undo(_("specify seektime"),
1217 perform_change_seektime_and_cleanup.call(values[:old])
1219 autoscroll_if_needed($autotable_sw, img, textview)
1220 $notebook.set_page(1)
1222 perform_change_seektime_and_cleanup.call(values[:new])
1224 autoscroll_if_needed($autotable_sw, img, textview)
1225 $notebook.set_page(1)
1230 change_seektime_and_cleanup = proc {
1231 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1232 change_seektime_and_cleanup_real.call(values)
1236 change_pano_amount_and_cleanup_real = proc { |values|
1237 perform_change_pano_amount_and_cleanup = proc { |val|
1238 cleanup_all_thumbnails.call
1239 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1241 perform_change_pano_amount_and_cleanup.call(values[:new])
1243 save_undo(_("change panorama amount"),
1245 perform_change_pano_amount_and_cleanup.call(values[:old])
1247 autoscroll_if_needed($autotable_sw, img, textview)
1248 $notebook.set_page(1)
1250 perform_change_pano_amount_and_cleanup.call(values[:new])
1252 autoscroll_if_needed($autotable_sw, img, textview)
1253 $notebook.set_page(1)
1258 change_pano_amount_and_cleanup = proc {
1259 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1260 change_pano_amount_and_cleanup_real.call(values)
1264 whitebalance_and_cleanup_real = proc { |values|
1265 perform_change_whitebalance_and_cleanup = proc { |val|
1266 cleanup_all_thumbnails.call
1267 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1268 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1269 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1271 perform_change_whitebalance_and_cleanup.call(values[:new])
1273 save_undo(_("fix white balance"),
1275 perform_change_whitebalance_and_cleanup.call(values[:old])
1277 autoscroll_if_needed($autotable_sw, img, textview)
1278 $notebook.set_page(1)
1280 perform_change_whitebalance_and_cleanup.call(values[:new])
1282 autoscroll_if_needed($autotable_sw, img, textview)
1283 $notebook.set_page(1)
1288 whitebalance_and_cleanup = proc {
1289 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1290 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1291 whitebalance_and_cleanup_real.call(values)
1295 gammacorrect_and_cleanup_real = proc { |values|
1296 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1297 cleanup_all_thumbnails.call
1298 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1299 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1300 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1302 perform_change_gammacorrect_and_cleanup.call(values[:new])
1304 save_undo(_("gamma correction"),
1306 perform_change_gammacorrect_and_cleanup.call(values[:old])
1308 autoscroll_if_needed($autotable_sw, img, textview)
1309 $notebook.set_page(1)
1311 perform_change_gammacorrect_and_cleanup.call(values[:new])
1313 autoscroll_if_needed($autotable_sw, img, textview)
1314 $notebook.set_page(1)
1319 gammacorrect_and_cleanup = Proc.new {
1320 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1321 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1322 gammacorrect_and_cleanup_real.call(values)
1326 enhance_and_cleanup = proc {
1327 perform_enhance_and_cleanup = proc {
1328 cleanup_all_thumbnails.call
1329 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1330 my_gen_real_thumbnail.call
1333 cleanup_all_thumbnails.call
1334 perform_enhance_and_cleanup.call
1336 save_undo(_("enhance"),
1338 perform_enhance_and_cleanup.call
1340 autoscroll_if_needed($autotable_sw, img, textview)
1341 $notebook.set_page(1)
1343 perform_enhance_and_cleanup.call
1345 autoscroll_if_needed($autotable_sw, img, textview)
1346 $notebook.set_page(1)
1351 delete = proc { |isacut|
1352 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 })
1355 perform_delete = proc {
1356 after = autotable.get_next_widget(vbox)
1358 after = autotable.get_previous_widget(vbox)
1360 if $config['deleteondisk'] && !isacut
1361 msg 3, "scheduling for delete: #{fullpath}"
1362 $todelete << fullpath
1364 autotable.remove_widget(vbox)
1366 $vbox2widgets[after][:textview].grab_focus
1367 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1371 previous_pos = autotable.get_current_number(vbox)
1375 delete_current_subalbum
1377 save_undo(_("delete"),
1379 autotable.reinsert(pos, vbox, filename)
1380 $notebook.set_page(1)
1381 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1383 msg 3, "removing deletion schedule of: #{fullpath}"
1384 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1387 $notebook.set_page(1)
1396 $cuts << { :vbox => vbox, :filename => filename }
1397 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1402 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1405 autotable.queue_draws << proc {
1406 $vbox2widgets[last[:vbox]][:textview].grab_focus
1407 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1409 save_undo(_("paste"),
1411 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1412 $notebook.set_page(1)
1415 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1417 $notebook.set_page(1)
1420 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1425 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1426 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1427 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1428 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1430 textview.signal_connect('key-press-event') { |w, event|
1433 x, y = autotable.get_current_pos(vbox)
1434 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1435 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1436 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1437 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1439 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1440 $vbox2widgets[widget_up][:textview].grab_focus
1447 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1449 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1450 $vbox2widgets[widget_down][:textview].grab_focus
1457 if event.keyval == Gdk::Keyval::GDK_Left
1460 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1467 rotate_and_cleanup.call(-90)
1470 if event.keyval == Gdk::Keyval::GDK_Right
1471 next_ = autotable.get_next_widget(vbox)
1472 if next_ && autotable.get_current_pos(next_)[0] > x
1474 $vbox2widgets[next_][:textview].grab_focus
1481 rotate_and_cleanup.call(90)
1484 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1487 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1488 view_element(filename, { :delete => delete })
1491 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1494 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1498 !propagate #- propagate if needed
1501 $ignore_next_release = false
1502 evtbox.signal_connect('button-press-event') { |w, event|
1503 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1504 if event.state & Gdk::Window::BUTTON3_MASK != 0
1505 #- gesture redo: hold right mouse button then click left mouse button
1506 $config['nogestures'] or perform_redo
1507 $ignore_next_release = true
1509 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1511 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1513 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1514 elsif $enhance.active?
1515 enhance_and_cleanup.call
1516 elsif $delete.active?
1520 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1523 $button1_pressed_autotable = true
1524 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1525 if event.state & Gdk::Window::BUTTON1_MASK != 0
1526 #- gesture undo: hold left mouse button then click right mouse button
1527 $config['nogestures'] or perform_undo
1528 $ignore_next_release = true
1530 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1531 view_element(filename, { :delete => delete })
1536 evtbox.signal_connect('button-release-event') { |w, event|
1537 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1538 if !$ignore_next_release
1539 x, y = autotable.get_current_pos(vbox)
1540 next_ = autotable.get_next_widget(vbox)
1541 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1542 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1543 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1544 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1545 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1546 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1547 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1549 $ignore_next_release = false
1550 $gesture_press = nil
1555 #- handle reordering with drag and drop
1556 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1557 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1558 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1559 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1562 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1564 #- mouse gesture first (dnd disables button-release-event)
1565 if $gesture_press && $gesture_press[:filename] == filename
1566 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1567 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1568 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1569 rotate_and_cleanup.call(angle)
1570 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1572 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1573 msg 3, "gesture delete: click-drag right button to the bottom"
1575 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1580 ctxt.targets.each { |target|
1581 if target.name == 'reorder-elements'
1582 move_dnd = proc { |from,to|
1585 autotable.move(from, to)
1586 save_undo(_("reorder"),
1589 autotable.move(to - 1, from)
1591 autotable.move(to, from + 1)
1593 $notebook.set_page(1)
1595 autotable.move(from, to)
1596 $notebook.set_page(1)
1601 if $multiple_dnd.size == 0
1602 move_dnd.call(selection_data.data.to_i,
1603 autotable.get_current_number(vbox))
1605 UndoHandler.begin_batch
1606 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1608 #- need to update current position between each call
1609 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1610 autotable.get_current_number(vbox))
1612 UndoHandler.end_batch
1623 def create_auto_table
1625 $autotable = Gtk::AutoTable.new(5)
1627 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1628 thumbnails_vb = Gtk::VBox.new(false, 5)
1630 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1631 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1632 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1633 thumbnails_vb.add($autotable)
1635 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1636 $autotable_sw.add_with_viewport(thumbnails_vb)
1638 #- follows stuff for handling multiple elements selection
1639 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1641 update_selected = proc {
1642 $autotable.current_order.each { |path|
1643 w = $name2widgets[path][:evtbox].window
1644 xm = w.position[0] + w.size[0]/2
1645 ym = w.position[1] + w.size[1]/2
1646 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1647 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1648 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1649 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1652 if $selected_elements[path] && ! $selected_elements[path][:keep]
1653 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))
1654 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1655 $selected_elements.delete(path)
1660 $autotable.signal_connect('realize') { |w,e|
1661 gc = Gdk::GC.new($autotable.window)
1662 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1663 gc.function = Gdk::GC::INVERT
1664 #- autoscroll handling for DND and multiple selections
1665 Gtk.timeout_add(100) {
1666 if ! $autotable.window.nil?
1667 w, x, y, mask = $autotable.window.pointer
1668 if mask & Gdk::Window::BUTTON1_MASK != 0
1669 if y < $autotable_sw.vadjustment.value
1671 $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]])
1673 if $button1_pressed_autotable || press_x
1674 scroll_upper($autotable_sw, y)
1677 w, pos_x, pos_y = $autotable.window.pointer
1678 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1679 update_selected.call
1682 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1684 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1686 if $button1_pressed_autotable || press_x
1687 scroll_lower($autotable_sw, y)
1690 w, pos_x, pos_y = $autotable.window.pointer
1691 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1692 update_selected.call
1697 ! $autotable.window.nil?
1701 $autotable.signal_connect('button-press-event') { |w,e|
1703 if !$button1_pressed_autotable
1706 if e.state & Gdk::Window::SHIFT_MASK == 0
1707 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1708 $selected_elements = {}
1709 $statusbar.push(0, utf8(_("Nothing selected.")))
1711 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1713 set_mousecursor(Gdk::Cursor::TCROSS)
1717 $autotable.signal_connect('button-release-event') { |w,e|
1719 if $button1_pressed_autotable
1720 #- unselect all only now
1721 $multiple_dnd = $selected_elements.keys
1722 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1723 $selected_elements = {}
1724 $button1_pressed_autotable = false
1727 $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]])
1728 if $selected_elements.length > 0
1729 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1732 press_x = press_y = pos_x = pos_y = nil
1733 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1737 $autotable.signal_connect('motion-notify-event') { |w,e|
1740 $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]])
1744 $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]])
1745 update_selected.call
1751 def create_subalbums_page
1753 subalbums_hb = Gtk::HBox.new
1754 $subalbums_vb = Gtk::VBox.new(false, 5)
1755 subalbums_hb.pack_start($subalbums_vb, false, false)
1756 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1757 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1758 $subalbums_sw.add_with_viewport(subalbums_hb)
1761 def save_current_file
1767 ios = File.open($filename, "w")
1768 $xmldoc.write(ios, 0)
1770 rescue Iconv::IllegalSequence
1771 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1772 if ! ios.nil? && ! ios.closed?
1775 $xmldoc.xml_decl.encoding = 'UTF-8'
1776 ios = File.open($filename, "w")
1777 $xmldoc.write(ios, 0)
1788 def save_current_file_user
1789 save_tempfilename = $filename
1790 $filename = $orig_filename
1791 if ! save_current_file
1792 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1793 $filename = save_tempfilename
1797 $generated_outofline = false
1798 $filename = save_tempfilename
1800 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1801 $todelete.each { |f|
1806 def mark_document_as_dirty
1807 $xmldoc.elements.each('//dir') { |elem|
1808 elem.delete_attribute('already-generated')
1812 #- ret: true => ok false => cancel
1813 def ask_save_modifications(msg1, msg2, *options)
1815 options = options.size > 0 ? options[0] : {}
1817 if options[:disallow_cancel]
1818 dialog = Gtk::Dialog.new(msg1,
1820 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1821 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1822 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1824 dialog = Gtk::Dialog.new(msg1,
1826 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1827 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1828 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1829 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1831 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1832 dialog.vbox.add(Gtk::Label.new(msg2))
1833 dialog.window_position = Gtk::Window::POS_CENTER
1836 dialog.run { |response|
1838 if response == Gtk::Dialog::RESPONSE_YES
1839 if ! save_current_file_user
1840 return ask_save_modifications(msg1, msg2, options)
1843 #- if we have generated an album but won't save modifications, we must remove
1844 #- already-generated markers in original file
1845 if $generated_outofline
1847 $xmldoc = REXML::Document.new File.new($orig_filename)
1848 mark_document_as_dirty
1849 ios = File.open($orig_filename, "w")
1850 $xmldoc.write(ios, 0)
1853 puts "exception: #{$!}"
1857 if response == Gtk::Dialog::RESPONSE_CANCEL
1860 $todelete = [] #- unconditionally clear the list of images/videos to delete
1866 def try_quit(*options)
1867 if ask_save_modifications(utf8(_("Save before quitting?")),
1868 utf8(_("Do you want to save your changes before quitting?")),
1874 def show_popup(parent, msg, *options)
1875 dialog = Gtk::Dialog.new
1876 if options[0] && options[0][:title]
1877 dialog.title = options[0][:title]
1879 dialog.title = utf8(_("Booh message"))
1881 lbl = Gtk::Label.new
1882 if options[0] && options[0][:nomarkup]
1887 if options[0] && options[0][:centered]
1888 lbl.set_justify(Gtk::Justification::CENTER)
1890 if options[0] && options[0][:selectable]
1891 lbl.selectable = true
1893 if options[0] && options[0][:topwidget]
1894 dialog.vbox.add(options[0][:topwidget])
1896 if options[0] && options[0][:scrolled]
1897 sw = Gtk::ScrolledWindow.new(nil, nil)
1898 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1899 sw.add_with_viewport(lbl)
1901 dialog.set_default_size(500, 600)
1903 dialog.vbox.add(lbl)
1904 dialog.set_default_size(200, 120)
1906 if options[0] && options[0][:okcancel]
1907 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1909 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1911 if options[0] && options[0][:pos_centered]
1912 dialog.window_position = Gtk::Window::POS_CENTER
1914 dialog.window_position = Gtk::Window::POS_MOUSE
1917 if options[0] && options[0][:linkurl]
1918 linkbut = Gtk::Button.new('')
1919 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1920 linkbut.signal_connect('clicked') {
1921 open_url(options[0][:linkurl] + '/index.html')
1922 dialog.response(Gtk::Dialog::RESPONSE_OK)
1923 set_mousecursor_normal
1925 linkbut.relief = Gtk::RELIEF_NONE
1926 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1927 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1928 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1933 if !options[0] || !options[0][:not_transient]
1934 dialog.transient_for = parent
1935 dialog.run { |response|
1937 if options[0] && options[0][:okcancel]
1938 return response == Gtk::Dialog::RESPONSE_OK
1942 dialog.signal_connect('response') { dialog.destroy }
1946 def backend_wait_message(parent, msg, infopipe_path, mode)
1948 w.set_transient_for(parent)
1951 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1952 vb.pack_start(Gtk::Label.new(msg), false, false)
1954 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1955 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1956 if mode != 'one dir scan'
1957 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1959 if mode == 'web-album'
1960 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1961 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1963 vb.pack_start(Gtk::HSeparator.new, false, false)
1965 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1966 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1967 vb.pack_end(bottom, false, false)
1969 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1970 refresh_thread = Thread.new {
1971 directories_counter = 0
1972 while line = infopipe.gets
1973 if line =~ /^directories: (\d+), sizes: (\d+)/
1974 directories = $1.to_f + 1
1976 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1977 elements = $3.to_f + 1
1978 if mode == 'web-album'
1982 gtk_thread_protect { pb1_1.fraction = 0 }
1983 if mode != 'one dir scan'
1984 newtext = utf8(full_src_dir_to_rel($1, $2))
1985 newtext = '/' if newtext == ''
1986 gtk_thread_protect { pb1_2.text = newtext }
1987 directories_counter += 1
1988 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1990 elsif line =~ /^processing element$/
1991 element_counter += 1
1992 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1993 elsif line =~ /^processing size$/
1994 element_counter += 1
1995 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1996 elsif line =~ /^finished processing sizes$/
1997 gtk_thread_protect { pb1_1.fraction = 1 }
1998 elsif line =~ /^creating index.html$/
1999 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2000 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2001 directories_counter = 0
2002 elsif line =~ /^index.html: (.+)\|(.+)/
2003 newtext = utf8(full_src_dir_to_rel($1, $2))
2004 newtext = '/' if newtext == ''
2005 gtk_thread_protect { pb2.text = newtext }
2006 directories_counter += 1
2007 gtk_thread_protect { pb2.fraction = directories_counter / directories }
2008 elsif line =~ /^die: (.*)$/
2015 w.signal_connect('delete-event') { w.destroy }
2016 w.signal_connect('destroy') {
2017 Thread.kill(refresh_thread)
2018 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2021 File.delete(infopipe_path)
2024 w.window_position = Gtk::Window::POS_CENTER
2030 def call_backend(cmd, waitmsg, mode, params)
2031 pipe = Tempfile.new("boohpipe")
2033 system("mkfifo #{pipe.path}")
2034 cmd += " --info-pipe #{pipe.path}"
2035 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2040 id, exitstatus = Process.waitpid2(pid)
2041 gtk_thread_protect { w8.destroy }
2043 if params[:successmsg]
2044 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2046 if params[:closure_after]
2047 gtk_thread_protect(¶ms[:closure_after])
2049 elsif exitstatus == 15
2050 #- say nothing, user aborted
2052 gtk_thread_protect { show_popup($main_window,
2053 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2059 button.signal_connect('clicked') {
2060 Process.kill('SIGTERM', pid)
2064 def save_changes(*forced)
2065 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2069 $xmldir.delete_attribute('already-generated')
2071 propagate_children = proc { |xmldir|
2072 if xmldir.attributes['subdirs-caption']
2073 xmldir.delete_attribute('already-generated')
2075 xmldir.elements.each('dir') { |element|
2076 propagate_children.call(element)
2080 if $xmldir.child_byname_notattr('dir', 'deleted')
2081 new_title = $subalbums_title.buffer.text
2082 if new_title != $xmldir.attributes['subdirs-caption']
2083 parent = $xmldir.parent
2084 if parent.name == 'dir'
2085 parent.delete_attribute('already-generated')
2087 propagate_children.call($xmldir)
2089 $xmldir.add_attribute('subdirs-caption', new_title)
2090 $xmldir.elements.each('dir') { |element|
2091 if !element.attributes['deleted']
2092 path = element.attributes['path']
2093 newtext = $subalbums_edits[path][:editzone].buffer.text
2094 if element.attributes['subdirs-caption']
2095 if element.attributes['subdirs-caption'] != newtext
2096 propagate_children.call(element)
2098 element.add_attribute('subdirs-caption', newtext)
2099 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2101 if element.attributes['thumbnails-caption'] != newtext
2102 element.delete_attribute('already-generated')
2104 element.add_attribute('thumbnails-caption', newtext)
2105 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2111 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2112 if $xmldir.attributes['thumbnails-caption']
2113 path = $xmldir.attributes['path']
2114 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2116 elsif $xmldir.attributes['thumbnails-caption']
2117 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2120 if $xmldir.attributes['thumbnails-caption']
2121 if edit = $subalbums_edits[$xmldir.attributes['path']]
2122 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2126 #- remove and reinsert elements to reflect new ordering
2129 $xmldir.elements.each { |element|
2130 if element.name == 'image' || element.name == 'video'
2131 saves[element.attributes['filename']] = element.remove
2135 $autotable.current_order.each { |path|
2136 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2137 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2140 saves.each_key { |path|
2141 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2142 chld.add_attribute('deleted', 'true')
2146 def sort_by_exif_date
2150 $xmldir.elements.each { |element|
2151 if element.name == 'image' || element.name == 'video'
2152 current_order << element.attributes['filename']
2156 #- look for EXIF dates
2159 if current_order.size > 20
2161 w.set_transient_for($main_window)
2163 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2164 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2165 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2166 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2167 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2168 vb.pack_end(bottom, false, false)
2170 w.signal_connect('delete-event') { w.destroy }
2171 w.window_position = Gtk::Window::POS_CENTER
2175 b.signal_connect('clicked') { aborted = true }
2177 current_order.each { |f|
2179 if entry2type(f) == 'image'
2181 pb.fraction = i.to_f / current_order.size
2182 Gtk.main_iteration while Gtk.events_pending?
2183 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2185 dates[f] = date_time
2198 current_order.each { |f|
2199 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2201 dates[f] = date_time
2207 $xmldir.elements.each { |element|
2208 if element.name == 'image' || element.name == 'video'
2209 saves[element.attributes['filename']] = element.remove
2213 neworder = smartsort(current_order, dates)
2216 $xmldir.add_element(saves[f].name, saves[f].attributes)
2219 #- let the auto-table reflect new ordering
2223 def remove_all_captions
2226 $autotable.current_order.each { |path|
2227 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2228 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2230 save_undo(_("remove all captions"),
2232 texts.each_key { |key|
2233 $name2widgets[key][:textview].buffer.text = texts[key]
2235 $notebook.set_page(1)
2237 texts.each_key { |key|
2238 $name2widgets[key][:textview].buffer.text = ''
2240 $notebook.set_page(1)
2246 $selected_elements.each_key { |path|
2247 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2253 $selected_elements = {}
2257 $undo_tb.sensitive = $undo_mb.sensitive = false
2258 $redo_tb.sensitive = $redo_mb.sensitive = false
2264 $subalbums_vb.children.each { |chld|
2265 $subalbums_vb.remove(chld)
2267 $subalbums = Gtk::Table.new(0, 0, true)
2268 current_y_sub_albums = 0
2270 $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"])
2271 $subalbums_edits = {}
2272 subalbums_counter = 0
2273 subalbums_edits_bypos = {}
2275 add_subalbum = proc { |xmldir, counter|
2276 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2277 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2278 if xmldir == $xmldir
2279 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2280 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2281 caption = xmldir.attributes['thumbnails-caption']
2282 infotype = 'thumbnails'
2284 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2285 captionfile, caption = find_subalbum_caption_info(xmldir)
2286 infotype = find_subalbum_info_type(xmldir)
2288 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2289 hbox = Gtk::HBox.new
2290 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2292 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2295 my_gen_real_thumbnail = proc {
2296 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2299 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2300 f.add(img = Gtk::Image.new)
2301 my_gen_real_thumbnail.call
2303 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2305 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2306 $subalbums.attach(hbox,
2307 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2309 frame, textview = create_editzone($subalbums_sw, 0, img)
2310 textview.buffer.text = caption
2311 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2312 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2314 change_image = proc {
2315 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2317 Gtk::FileChooser::ACTION_OPEN,
2319 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2320 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2321 fc.transient_for = $main_window
2322 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))
2323 f.add(preview_img = Gtk::Image.new)
2325 fc.signal_connect('update-preview') { |w|
2327 if fc.preview_filename
2328 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2329 fc.preview_widget_active = true
2331 rescue Gdk::PixbufError
2332 fc.preview_widget_active = false
2335 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2337 old_file = captionfile
2338 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2339 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2340 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2341 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2343 new_file = fc.filename
2344 msg 3, "new captionfile is: #{fc.filename}"
2345 perform_changefile = proc {
2346 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2347 $modified_pixbufs.delete(thumbnail_file)
2348 xmldir.delete_attribute("#{infotype}-rotate")
2349 xmldir.delete_attribute("#{infotype}-color-swap")
2350 xmldir.delete_attribute("#{infotype}-enhance")
2351 xmldir.delete_attribute("#{infotype}-seektime")
2352 my_gen_real_thumbnail.call
2354 perform_changefile.call
2356 save_undo(_("change caption file for sub-album"),
2358 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2359 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2360 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2361 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2362 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2363 my_gen_real_thumbnail.call
2364 $notebook.set_page(0)
2366 perform_changefile.call
2367 $notebook.set_page(0)
2375 File.delete(thumbnail_file)
2376 my_gen_real_thumbnail.call
2379 rotate_and_cleanup = proc { |angle|
2380 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2381 File.delete(thumbnail_file)
2384 move = proc { |direction|
2387 save_changes('forced')
2388 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2389 if direction == 'up'
2390 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2391 subalbums_edits_bypos[oldpos - 1][:position] += 1
2393 if direction == 'down'
2394 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2395 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2397 if direction == 'top'
2398 for i in 1 .. oldpos - 1
2399 subalbums_edits_bypos[i][:position] += 1
2401 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2403 if direction == 'bottom'
2404 for i in oldpos + 1 .. subalbums_counter
2405 subalbums_edits_bypos[i][:position] -= 1
2407 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2411 $xmldir.elements.each('dir') { |element|
2412 if (!element.attributes['deleted'])
2413 elems << [ element.attributes['path'], element.remove ]
2416 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2417 each { |e| $xmldir.add_element(e[1]) }
2418 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2419 $xmldir.elements.each('descendant::dir') { |elem|
2420 elem.delete_attribute('already-generated')
2423 sel = $albums_tv.selection.selected_rows
2425 populate_subalbums_treeview(false)
2426 $albums_tv.selection.select_path(sel[0])
2429 color_swap_and_cleanup = proc {
2430 perform_color_swap_and_cleanup = proc {
2431 color_swap(xmldir, "#{infotype}-")
2432 my_gen_real_thumbnail.call
2434 perform_color_swap_and_cleanup.call
2436 save_undo(_("color swap"),
2438 perform_color_swap_and_cleanup.call
2439 $notebook.set_page(0)
2441 perform_color_swap_and_cleanup.call
2442 $notebook.set_page(0)
2447 change_seektime_and_cleanup = proc {
2448 if values = ask_new_seektime(xmldir, "#{infotype}-")
2449 perform_change_seektime_and_cleanup = proc { |val|
2450 change_seektime(xmldir, "#{infotype}-", val)
2451 my_gen_real_thumbnail.call
2453 perform_change_seektime_and_cleanup.call(values[:new])
2455 save_undo(_("specify seektime"),
2457 perform_change_seektime_and_cleanup.call(values[:old])
2458 $notebook.set_page(0)
2460 perform_change_seektime_and_cleanup.call(values[:new])
2461 $notebook.set_page(0)
2467 whitebalance_and_cleanup = proc {
2468 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2469 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2470 perform_change_whitebalance_and_cleanup = proc { |val|
2471 change_whitebalance(xmldir, "#{infotype}-", val)
2472 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2473 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2474 File.delete(thumbnail_file)
2476 perform_change_whitebalance_and_cleanup.call(values[:new])
2478 save_undo(_("fix white balance"),
2480 perform_change_whitebalance_and_cleanup.call(values[:old])
2481 $notebook.set_page(0)
2483 perform_change_whitebalance_and_cleanup.call(values[:new])
2484 $notebook.set_page(0)
2490 gammacorrect_and_cleanup = proc {
2491 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2492 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2493 perform_change_gammacorrect_and_cleanup = proc { |val|
2494 change_gammacorrect(xmldir, "#{infotype}-", val)
2495 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2496 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2497 File.delete(thumbnail_file)
2499 perform_change_gammacorrect_and_cleanup.call(values[:new])
2501 save_undo(_("gamma correction"),
2503 perform_change_gammacorrect_and_cleanup.call(values[:old])
2504 $notebook.set_page(0)
2506 perform_change_gammacorrect_and_cleanup.call(values[:new])
2507 $notebook.set_page(0)
2513 enhance_and_cleanup = proc {
2514 perform_enhance_and_cleanup = proc {
2515 enhance(xmldir, "#{infotype}-")
2516 my_gen_real_thumbnail.call
2519 perform_enhance_and_cleanup.call
2521 save_undo(_("enhance"),
2523 perform_enhance_and_cleanup.call
2524 $notebook.set_page(0)
2526 perform_enhance_and_cleanup.call
2527 $notebook.set_page(0)
2532 evtbox.signal_connect('button-press-event') { |w, event|
2533 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2535 rotate_and_cleanup.call(90)
2537 rotate_and_cleanup.call(-90)
2538 elsif $enhance.active?
2539 enhance_and_cleanup.call
2542 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2543 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2544 { :forbid_left => true, :forbid_right => true,
2545 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2546 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2547 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2548 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2549 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2551 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2556 evtbox.signal_connect('button-press-event') { |w, event|
2557 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2561 evtbox.signal_connect('button-release-event') { |w, event|
2562 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2563 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2564 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2565 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2566 msg 3, "gesture rotate: #{angle}"
2567 rotate_and_cleanup.call(angle)
2570 $gesture_press = nil
2573 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2574 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2575 current_y_sub_albums += 1
2578 if $xmldir.child_byname_notattr('dir', 'deleted')
2580 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2581 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2582 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2583 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2584 #- this album image/caption
2585 if $xmldir.attributes['thumbnails-caption']
2586 add_subalbum.call($xmldir, 0)
2589 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2590 $xmldir.elements.each { |element|
2591 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2592 #- element (image or video) of this album
2593 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2594 msg 3, "dest_img: #{dest_img}"
2595 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2596 total[element.name] += 1
2598 if element.name == 'dir' && !element.attributes['deleted']
2599 #- sub-album image/caption
2600 add_subalbum.call(element, subalbums_counter += 1)
2601 total[element.name] += 1
2604 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2605 total['image'], total['video'], total['dir'] ]))
2606 $subalbums_vb.add($subalbums)
2607 $subalbums_vb.show_all
2609 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2610 $notebook.get_tab_label($autotable_sw).sensitive = false
2611 $notebook.set_page(0)
2612 $thumbnails_title.buffer.text = ''
2614 $notebook.get_tab_label($autotable_sw).sensitive = true
2615 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2618 if !$xmldir.child_byname_notattr('dir', 'deleted')
2619 $notebook.get_tab_label($subalbums_sw).sensitive = false
2620 $notebook.set_page(1)
2622 $notebook.get_tab_label($subalbums_sw).sensitive = true
2626 def pixbuf_or_nil(filename)
2628 return Gdk::Pixbuf.new(filename)
2634 def theme_choose(current)
2635 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2637 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2638 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2639 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2641 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2642 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2643 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2644 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2645 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2646 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2647 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2648 treeview.signal_connect('button-press-event') { |w, event|
2649 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2650 dialog.response(Gtk::Dialog::RESPONSE_OK)
2654 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2656 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2659 iter[0] = File.basename(dir)
2660 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2661 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2662 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2663 if File.basename(dir) == current
2664 treeview.selection.select_iter(iter)
2668 dialog.set_default_size(700, 400)
2669 dialog.vbox.show_all
2670 dialog.run { |response|
2671 iter = treeview.selection.selected
2673 if response == Gtk::Dialog::RESPONSE_OK && iter
2674 return model.get_value(iter, 0)
2680 def show_password_protections
2681 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2682 child_iter = $albums_iters[xmldir.attributes['path']]
2683 if xmldir.attributes['password-protect']
2684 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2685 already_protected = true
2686 elsif already_protected
2687 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2689 pix = pix.saturate_and_pixelate(1, true)
2695 xmldir.elements.each('dir') { |elem|
2696 if !elem.attributes['deleted']
2697 examine_dir_elem.call(child_iter, elem, already_protected)
2701 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2704 def populate_subalbums_treeview(select_first)
2708 $subalbums_vb.children.each { |chld|
2709 $subalbums_vb.remove(chld)
2712 source = $xmldoc.root.attributes['source']
2713 msg 3, "source: #{source}"
2715 xmldir = $xmldoc.elements['//dir']
2716 if !xmldir || xmldir.attributes['path'] != source
2717 msg 1, _("Corrupted booh file...")
2721 append_dir_elem = proc { |parent_iter, xmldir|
2722 child_iter = $albums_ts.append(parent_iter)
2723 child_iter[0] = File.basename(xmldir.attributes['path'])
2724 child_iter[1] = xmldir.attributes['path']
2725 $albums_iters[xmldir.attributes['path']] = child_iter
2726 msg 3, "puttin location: #{xmldir.attributes['path']}"
2727 xmldir.elements.each('dir') { |elem|
2728 if !elem.attributes['deleted']
2729 append_dir_elem.call(child_iter, elem)
2733 append_dir_elem.call(nil, xmldir)
2734 show_password_protections
2736 $albums_tv.expand_all
2738 $albums_tv.selection.select_iter($albums_ts.iter_first)
2742 def select_current_theme
2743 select_theme($xmldoc.root.attributes['theme'],
2744 $xmldoc.root.attributes['limit-sizes'],
2745 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2746 $xmldoc.root.attributes['thumbnails-per-row'])
2749 def open_file(filename)
2753 $current_path = nil #- invalidate
2754 $modified_pixbufs = {}
2757 $subalbums_vb.children.each { |chld|
2758 $subalbums_vb.remove(chld)
2761 if !File.exists?(filename)
2762 return utf8(_("File not found."))
2766 $xmldoc = REXML::Document.new File.new(filename)
2771 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2772 if entry2type(filename).nil?
2773 return utf8(_("Not a booh file!"))
2775 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."))
2779 if !source = $xmldoc.root.attributes['source']
2780 return utf8(_("Corrupted booh file..."))
2783 if !dest = $xmldoc.root.attributes['destination']
2784 return utf8(_("Corrupted booh file..."))
2787 if !theme = $xmldoc.root.attributes['theme']
2788 return utf8(_("Corrupted booh file..."))
2791 if $xmldoc.root.attributes['version'] < '0.8.99.2'
2792 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2793 mark_document_as_dirty
2794 if $xmldoc.root.attributes['version'] < '0.8.4'
2795 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2796 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2797 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2798 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2799 if old_dest_dir != new_dest_dir
2800 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2802 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2803 xmldir.elements.each { |element|
2804 if %w(image video).include?(element.name) && !element.attributes['deleted']
2805 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2806 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2807 Dir[old_name + '*'].each { |file|
2808 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2809 file != new_file and sys("mv '#{file}' '#{new_file}'")
2812 if element.name == 'dir' && !element.attributes['deleted']
2813 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2814 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2815 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2819 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2823 $xmldoc.root.add_attribute('version', $VERSION)
2826 select_current_theme
2828 $filename = filename
2829 $default_size['thumbnails'] =~ /(.*)x(.*)/
2830 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2831 $albums_thumbnail_size =~ /(.*)x(.*)/
2832 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2834 populate_subalbums_treeview(true)
2836 $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
2840 def open_file_user(filename)
2841 result = open_file(filename)
2843 $config['last-opens'] ||= []
2844 if $config['last-opens'][-1] != utf8(filename)
2845 $config['last-opens'] << utf8(filename)
2847 $orig_filename = $filename
2848 tmp = Tempfile.new("boohtemp")
2851 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2853 $tempfiles << $filename << "#{$filename}.backup"
2855 $orig_filename = nil
2861 if !ask_save_modifications(utf8(_("Save this album?")),
2862 utf8(_("Do you want to save the changes to this album?")),
2863 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2866 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2868 Gtk::FileChooser::ACTION_OPEN,
2870 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2871 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2872 fc.set_current_folder(File.expand_path("~/.booh"))
2873 fc.transient_for = $main_window
2876 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2877 push_mousecursor_wait(fc)
2878 msg = open_file_user(fc.filename)
2893 def additional_booh_options
2896 options += "--mproc #{$config['mproc'].to_i} "
2898 options += "--comments-format '#{$config['comments-format']}'"
2903 if !ask_save_modifications(utf8(_("Save this album?")),
2904 utf8(_("Do you want to save the changes to this album?")),
2905 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2908 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2910 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2911 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2912 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2914 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2915 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2916 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2917 tbl.attach(src = Gtk::Entry.new,
2918 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2919 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2920 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2921 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2922 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2923 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2924 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2925 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2926 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2927 tbl.attach(dest = Gtk::Entry.new,
2928 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2929 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2930 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2931 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2932 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2933 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2934 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2935 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2936 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2938 tooltips = Gtk::Tooltips.new
2939 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2940 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2941 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2942 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2943 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2944 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2945 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)
2946 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2947 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2948 nperpage_model = Gtk::ListStore.new(String, String)
2949 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
2950 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
2951 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
2952 nperpagecombo.set_attributes(crt, { :markup => 0 })
2953 iter = nperpage_model.append
2954 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
2956 [ 12, 20, 30, 40, 50 ].each { |v|
2957 iter = nperpage_model.append
2958 iter[0] = iter[1] = v.to_s
2960 nperpagecombo.active = 0
2961 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
2962 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
2963 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)
2964 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2965 pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
2966 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)
2968 src_nb_calculated_for = ''
2970 process_src_nb = proc {
2971 if src.text != src_nb_calculated_for
2972 src_nb_calculated_for = src.text
2974 Thread.kill(src_nb_thread)
2977 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
2978 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
2980 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2981 if File.readable?(from_utf8_safe(src_nb_calculated_for))
2982 src_nb_thread = Thread.new {
2983 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2984 total = { 'image' => 0, 'video' => 0, nil => 0 }
2985 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2986 if File.basename(dir) =~ /^\./
2990 Dir.entries(dir.chomp).each { |file|
2991 total[entry2type(file)] += 1
2993 rescue Errno::EACCES, Errno::ENOENT
2997 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3001 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3004 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3010 timeout_src_nb = Gtk.timeout_add(100) {
3014 src_browse.signal_connect('clicked') {
3015 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
3017 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3019 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3020 fc.transient_for = $main_window
3021 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3022 src.text = utf8(fc.filename)
3024 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3029 dest_browse.signal_connect('clicked') {
3030 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3032 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3034 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3035 fc.transient_for = $main_window
3036 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3037 dest.text = utf8(fc.filename)
3042 conf_browse.signal_connect('clicked') {
3043 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3045 Gtk::FileChooser::ACTION_SAVE,
3047 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3048 fc.transient_for = $main_window
3049 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3050 fc.set_current_folder(File.expand_path("~/.booh"))
3051 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3052 conf.text = utf8(fc.filename)
3059 recreate_theme_config = proc {
3060 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3062 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3063 $images_size.each { |s|
3064 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3068 tooltips.set_tip(cb, utf8(s['description']), nil)
3069 theme_sizes << { :widget => cb, :value => s['name'] }
3071 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3072 tooltips = Gtk::Tooltips.new
3073 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3074 theme_sizes << { :widget => cb, :value => 'original' }
3077 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3080 $allowed_N_values.each { |n|
3082 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3084 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3086 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3090 nperrows << { :widget => rb, :value => n }
3092 nperrowradios.show_all
3094 recreate_theme_config.call
3096 theme_button.signal_connect('clicked') {
3097 if newtheme = theme_choose(theme_button.label)
3098 theme_button.label = newtheme
3099 recreate_theme_config.call
3103 dialog.vbox.add(frame1)
3104 dialog.vbox.add(frame2)
3110 dialog.run { |response|
3111 if response == Gtk::Dialog::RESPONSE_OK
3112 srcdir = from_utf8_safe(src.text)
3113 destdir = from_utf8_safe(dest.text)
3114 confpath = from_utf8_safe(conf.text)
3115 if src.text != '' && srcdir == ''
3116 show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3118 elsif !File.directory?(srcdir)
3119 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3121 elsif dest.text != '' && destdir == ''
3122 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3124 elsif destdir != make_dest_filename(destdir)
3125 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3127 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3128 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
3130 elsif File.exists?(destdir) && !File.directory?(destdir)
3131 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3133 elsif conf.text == ''
3134 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3136 elsif conf.text != '' && confpath == ''
3137 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3139 elsif File.directory?(confpath)
3140 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3142 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3143 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3145 system("mkdir '#{destdir}'")
3146 if !File.directory?(destdir)
3147 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3159 srcdir = from_utf8(src.text)
3160 destdir = from_utf8(dest.text)
3161 configskel = File.expand_path(from_utf8(conf.text))
3162 theme = theme_button.label
3163 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3164 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3165 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3166 opt432 = optimize432.active?
3167 madewith = madewithentry.text.gsub('"', '"').gsub('\'', ''')
3168 indexlink = indexlinkentry.text.gsub('"', '"').gsub('\'', ''')
3171 Thread.kill(src_nb_thread)
3172 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3175 Gtk.timeout_remove(timeout_src_nb)
3178 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3179 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3180 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3181 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3182 utf8(_("Please wait while scanning source directory...")),
3184 { :closure_after => proc { open_file_user(configskel) } })
3189 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3191 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3192 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3193 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3195 source = $xmldoc.root.attributes['source']
3196 dest = $xmldoc.root.attributes['destination']
3197 theme = $xmldoc.root.attributes['theme']
3198 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3199 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3200 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3201 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3203 limit_sizes = limit_sizes.split(/,/)
3205 madewith = ($xmldoc.root.attributes['made-with'] || '').gsub(''', '\'')
3206 indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub(''', '\'')
3208 tooltips = Gtk::Tooltips.new
3209 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3210 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3211 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3212 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3213 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3214 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3215 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3216 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3217 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3218 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3219 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3220 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3221 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3223 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3224 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3225 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3226 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3227 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3228 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3229 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)
3230 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3231 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3232 nperpage_model = Gtk::ListStore.new(String, String)
3233 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3234 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3235 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3236 nperpagecombo.set_attributes(crt, { :markup => 0 })
3237 iter = nperpage_model.append
3238 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3240 [ 12, 20, 30, 40, 50 ].each { |v|
3241 iter = nperpage_model.append
3242 iter[0] = iter[1] = v.to_s
3243 if nperpage && nperpage == v.to_s
3244 nperpagecombo.active_iter = iter
3247 if nperpagecombo.active_iter.nil?
3248 nperpagecombo.active = 0
3251 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3252 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3254 indexlinkentry.text = indexlink
3256 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)
3257 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3258 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3260 madewithentry.text = madewith
3262 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)
3266 recreate_theme_config = proc {
3267 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3269 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3271 $images_size.each { |s|
3272 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3274 if limit_sizes.include?(s['name'])
3282 tooltips.set_tip(cb, utf8(s['description']), nil)
3283 theme_sizes << { :widget => cb, :value => s['name'] }
3285 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3286 tooltips = Gtk::Tooltips.new
3287 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3288 if limit_sizes && limit_sizes.include?('original')
3291 theme_sizes << { :widget => cb, :value => 'original' }
3294 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3297 $allowed_N_values.each { |n|
3299 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3301 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3303 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3304 nperrowradios.add(Gtk::Label.new(' '))
3305 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3308 nperrows << { :widget => rb, :value => n.to_s }
3310 nperrowradios.show_all
3312 recreate_theme_config.call
3314 theme_button.signal_connect('clicked') {
3315 if newtheme = theme_choose(theme_button.label)
3318 theme_button.label = newtheme
3319 recreate_theme_config.call
3323 dialog.vbox.add(frame1)
3324 dialog.vbox.add(frame2)
3330 dialog.run { |response|
3331 if response == Gtk::Dialog::RESPONSE_OK
3332 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3333 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3342 save_theme = theme_button.label
3343 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3344 save_opt432 = optimize432.active?
3345 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3346 save_nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3347 save_madewith = madewithentry.text.gsub('"', '"').gsub('\'', ''')
3348 save_indexlink = indexlinkentry.text.gsub('"', '"').gsub('\'', ''')
3351 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_nperpage != nperpage || save_madewith != madewith || save_indexlink != indexlinkentry)
3352 mark_document_as_dirty
3354 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3355 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3356 (save_nperpage ? "--thumbnails-per-page #{save_nperpage} " : '') +
3357 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3358 utf8(_("Please wait while scanning source directory...")),
3360 { :closure_after => proc {
3361 open_file($filename)
3365 #- select_theme merges global variables, need to return to current choices
3366 select_current_theme
3373 sel = $albums_tv.selection.selected_rows
3375 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3376 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3377 utf8(_("Please wait while scanning source directory...")),
3379 { :closure_after => proc {
3380 open_file($filename)
3381 $albums_tv.selection.select_path(sel[0])
3389 sel = $albums_tv.selection.selected_rows
3391 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3392 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3393 utf8(_("Please wait while scanning source directory...")),
3395 { :closure_after => proc {
3396 open_file($filename)
3397 $albums_tv.selection.select_path(sel[0])
3405 theme = $xmldoc.root.attributes['theme']
3406 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3408 limit_sizes = "--sizes #{limit_sizes}"
3410 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3411 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3412 utf8(_("Please wait while scanning source directory...")),
3414 { :closure_after => proc {
3415 open_file($filename)
3421 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3423 Gtk::FileChooser::ACTION_SAVE,
3425 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3426 fc.transient_for = $main_window
3427 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3428 fc.set_current_folder(File.expand_path("~/.booh"))
3429 fc.filename = $orig_filename
3430 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3431 $orig_filename = fc.filename
3432 if ! save_current_file_user
3436 $config['last-opens'] ||= []
3437 $config['last-opens'] << $orig_filename
3443 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3445 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3446 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3447 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3449 dialog.vbox.add(notebook = Gtk::Notebook.new)
3450 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3451 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3452 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3453 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer']).set_size_request(250, -1)),
3454 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3455 tooltips = Gtk::Tooltips.new
3456 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3457 for example: /usr/bin/mplayer %f")), nil)
3458 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3459 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3460 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3461 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3462 tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3463 for example: /usr/bin/gimp-remote %f")), nil)
3464 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3465 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3466 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3467 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3468 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3469 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3470 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3471 0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3472 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(smp_hbox = Gtk::HBox.new.add(smp_spin = Gtk::SpinButton.new(2, 16, 1)).add(Gtk::Label.new(utf8(_("processors")))).set_sensitive(false)),
3473 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3474 tooltips.set_tip(smp_check, utf8(_("When activated, this option allows the thumbnails creation to run faster. However, if you don't have a multi-processor machine, this will only slow down processing!")), nil)
3475 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3476 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3477 tooltips.set_tip(nogestures_check, utf8(_("Mouse gestures are 'unusual' mouse movements triggering special actions, and are great for speeding up your editions. Get details on available mouse gestures from the Help menu.")), nil)
3478 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3479 0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3480 tooltips.set_tip(deleteondisk_check, utf8(_("Normally, deleting an image or video in booh only removes it from the web-album. If you check this option, the original file in source directory will be removed as well. Undo is possible, since actual deletion is performed only when web-album is saved.")), nil)
3482 smp_check.signal_connect('toggled') {
3483 if smp_check.active?
3484 smp_hbox.sensitive = true
3486 smp_hbox.sensitive = false
3490 smp_check.active = true
3491 smp_spin.value = $config['mproc'].to_i
3493 nogestures_check.active = $config['nogestures']
3494 deleteondisk_check.active = $config['deleteondisk']
3496 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3497 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3498 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3499 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),