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|
210 def set_mousecursor(what, *widget)
211 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
212 if widget[0] && widget[0].window
213 widget[0].window.cursor = cursor
215 if $main_window && $main_window.window
216 $main_window.window.cursor = cursor
218 $current_cursor = what
220 def set_mousecursor_wait(*widget)
221 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
222 if Thread.current == Thread.main
223 Gtk.main_iteration while Gtk.events_pending?
226 def set_mousecursor_normal(*widget)
227 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
229 def push_mousecursor_wait(*widget)
230 if $current_cursor != Gdk::Cursor::WATCH
231 $save_cursor = $current_cursor
232 gtk_thread_protect { set_mousecursor_wait(*widget) }
235 def pop_mousecursor(*widget)
236 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
240 source = $xmldoc.root.attributes['source']
241 dest = $xmldoc.root.attributes['destination']
242 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
245 def full_src_dir_to_rel(path, source)
246 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
249 def build_full_dest_filename(filename)
250 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
253 def save_undo(name, closure, *params)
254 UndoHandler.save_undo(name, closure, [ *params ])
255 $undo_tb.sensitive = $undo_mb.sensitive = true
256 $redo_tb.sensitive = $redo_mb.sensitive = false
259 def view_element(filename, closures)
260 if entry2type(filename) == 'video'
261 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
267 w = Gtk::Window.new.set_title(filename)
269 msg 3, "filename: #{filename}"
270 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
271 #- typically this file won't exist in case of videos; try with the largest thumbnail around
272 if !File.exists?(dest_img)
273 if entry2type(filename) == 'video'
274 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
275 if not alternatives.empty?
276 dest_img = alternatives[-1]
279 push_mousecursor_wait
280 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
282 if !File.exists?(dest_img)
283 msg 2, _("Could not generate fullscreen thumbnail!")
288 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)))
289 evt.signal_connect('button-press-event') { |this, event|
290 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
291 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
293 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
295 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
296 delete_item.signal_connect('activate') {
298 closures[:delete].call(false)
301 menu.popup(nil, nil, event.button, event.time)
304 evt.signal_connect('button-release-event') { |this, event|
306 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
307 msg 3, "gesture delete: click-drag right button to the bottom"
309 closures[:delete].call(false)
310 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
314 tooltips = Gtk::Tooltips.new
315 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
317 w.signal_connect('key-press-event') { |w,event|
318 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
320 closures[:delete].call(false)
324 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
325 b.signal_connect('clicked') { w.destroy }
328 vb.pack_start(evt, false, false)
329 vb.pack_end(bottom, false, false)
332 w.signal_connect('delete-event') { w.destroy }
333 w.window_position = Gtk::Window::POS_CENTER
337 def scroll_upper(scrolledwindow, ypos_top)
338 newval = scrolledwindow.vadjustment.value -
339 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
340 if newval < scrolledwindow.vadjustment.lower
341 newval = scrolledwindow.vadjustment.lower
343 scrolledwindow.vadjustment.value = newval
346 def scroll_lower(scrolledwindow, ypos_bottom)
347 newval = scrolledwindow.vadjustment.value +
348 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
349 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
350 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
352 scrolledwindow.vadjustment.value = newval
355 def autoscroll_if_needed(scrolledwindow, image, textview)
356 #- autoscroll if cursor or image is not visible, if possible
357 if image && image.window || textview.window
358 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
359 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
360 current_miny_visible = scrolledwindow.vadjustment.value
361 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
362 if ypos_top < current_miny_visible
363 scroll_upper(scrolledwindow, ypos_top)
364 elsif ypos_bottom > current_maxy_visible
365 scroll_lower(scrolledwindow, ypos_bottom)
370 def create_editzone(scrolledwindow, pagenum, image)
371 frame = Gtk::Frame.new
372 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
373 frame.set_shadow_type(Gtk::SHADOW_IN)
374 textview.signal_connect('key-press-event') { |w, event|
375 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
376 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
377 scrolledwindow.signal_emit('key-press-event', event)
379 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
380 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
381 if event.keyval == Gdk::Keyval::GDK_Up
382 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
383 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
385 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
388 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
389 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
391 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
398 candidate_undo_text = nil
399 textview.signal_connect('focus-in-event') { |w, event|
400 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
401 candidate_undo_text = textview.buffer.text
405 textview.signal_connect('key-release-event') { |w, event|
406 if candidate_undo_text && candidate_undo_text != textview.buffer.text
408 save_undo(_("text edit"),
410 save_text = textview.buffer.text
411 textview.buffer.text = text
413 $notebook.set_page(pagenum)
415 textview.buffer.text = save_text
417 $notebook.set_page(pagenum)
419 }, candidate_undo_text)
420 candidate_undo_text = nil
423 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)
424 autoscroll_if_needed(scrolledwindow, image, textview)
429 return [ frame, textview ]
432 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
434 if !$modified_pixbufs[thumbnail_img]
435 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
436 elsif !$modified_pixbufs[thumbnail_img][:orig]
437 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
440 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
443 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
444 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
445 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
446 if pixbuf.height > desired_y
447 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
448 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
449 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
454 if $modified_pixbufs[thumbnail_img][:whitebalance]
455 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
458 #- fix gamma correction
459 if $modified_pixbufs[thumbnail_img][:gammacorrect]
460 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
463 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
466 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
469 #- update rotate attribute
470 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
471 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
473 if $config['rotate-set-exif'] == 'true'
474 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
477 $modified_pixbufs[thumbnail_img] ||= {}
478 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
479 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
481 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
484 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
487 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
489 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
491 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
492 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
494 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
495 $notebook.set_page(0)
496 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
501 def color_swap(xmldir, attributes_prefix)
503 if xmldir.attributes["#{attributes_prefix}color-swap"]
504 xmldir.delete_attribute("#{attributes_prefix}color-swap")
506 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
510 def enhance(xmldir, attributes_prefix)
512 if xmldir.attributes["#{attributes_prefix}enhance"]
513 xmldir.delete_attribute("#{attributes_prefix}enhance")
515 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
519 def change_seektime(xmldir, attributes_prefix, value)
521 xmldir.add_attribute("#{attributes_prefix}seektime", value)
524 def ask_new_seektime(xmldir, attributes_prefix)
526 value = xmldir.attributes["#{attributes_prefix}seektime"]
531 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
533 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
534 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
535 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
539 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
543 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
544 entry.signal_connect('key-press-event') { |w, event|
545 if event.keyval == Gdk::Keyval::GDK_Return
546 dialog.response(Gtk::Dialog::RESPONSE_OK)
548 elsif event.keyval == Gdk::Keyval::GDK_Escape
549 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
552 false #- propagate if needed
556 dialog.window_position = Gtk::Window::POS_MOUSE
559 dialog.run { |response|
562 if response == Gtk::Dialog::RESPONSE_OK
564 msg 3, "changing seektime to #{newval}"
565 return { :old => value, :new => newval }
572 def change_pano_amount(xmldir, attributes_prefix, value)
575 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
577 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
581 def ask_new_pano_amount(xmldir, attributes_prefix)
583 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
588 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
590 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
591 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
592 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
596 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
597 of this panorama image compared to other regular images. For example, if the panorama
598 was taken out of four photos on one row, counting the necessary overlap, the width of
599 this panorama image should probably be roughly three times the width of regular images.
601 With this information, booh will be able to generate panorama thumbnails looking
605 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)")))).
606 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
607 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
608 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
609 spin.signal_connect('value-changed') {
612 dialog.window_position = Gtk::Window::POS_MOUSE
615 spin.value = value.to_f
622 dialog.run { |response|
626 newval = spin.value.to_f
629 if response == Gtk::Dialog::RESPONSE_OK
631 msg 3, "changing panorama amount to #{newval}"
632 return { :old => value, :new => newval }
639 def change_whitebalance(xmlelem, attributes_prefix, value)
641 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
644 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
646 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
647 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
648 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
649 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
650 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
651 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
652 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
653 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
654 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
655 $modified_pixbufs[thumbnail_img] ||= {}
656 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
657 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
659 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
660 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
662 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
665 $modified_pixbufs[thumbnail_img] ||= {}
666 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
668 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
671 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
672 #- init $modified_pixbufs correctly
673 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
675 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
677 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
679 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
680 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
681 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
685 _("You can fix the <b>white balance</b> of the image, if your image is too blue
686 or too yellow because your camera didn't detect the light correctly. Drag the
687 slider below the image to the left for more blue, to the right for more yellow.
691 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
693 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
695 dialog.window_position = Gtk::Window::POS_MOUSE
699 timeout = Gtk.timeout_add(100) {
700 if hs.value != lastval
703 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
709 dialog.run { |response|
710 Gtk.timeout_remove(timeout)
711 if response == Gtk::Dialog::RESPONSE_OK
713 newval = hs.value.to_s
714 msg 3, "changing white balance to #{newval}"
716 return { :old => value, :new => newval }
719 $modified_pixbufs[thumbnail_img] ||= {}
720 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
721 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
729 def change_gammacorrect(xmlelem, attributes_prefix, value)
731 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
734 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
736 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
737 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
738 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
739 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
740 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
741 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
742 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
743 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
744 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
745 $modified_pixbufs[thumbnail_img] ||= {}
746 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
747 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
749 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
750 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
752 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
755 $modified_pixbufs[thumbnail_img] ||= {}
756 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
758 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
761 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
762 #- init $modified_pixbufs correctly
763 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
765 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
767 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
769 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
770 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
771 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
775 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
776 or too bright. Drag the slider below the image.
780 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
782 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
784 dialog.window_position = Gtk::Window::POS_MOUSE
788 timeout = Gtk.timeout_add(100) {
789 if hs.value != lastval
792 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
798 dialog.run { |response|
799 Gtk.timeout_remove(timeout)
800 if response == Gtk::Dialog::RESPONSE_OK
802 newval = hs.value.to_s
803 msg 3, "gamma correction to #{newval}"
805 return { :old => value, :new => newval }
808 $modified_pixbufs[thumbnail_img] ||= {}
809 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
810 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
818 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
819 system("rm -f '#{destfile}'")
820 #- type can be 'element' or 'subdir'
822 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
824 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
828 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
830 push_mousecursor_wait
831 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
834 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
840 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
841 distribute_multiple_call = Proc.new { |action, arg|
842 $selected_elements.each_key { |path|
843 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
845 if possible_actions[:can_multiple] && $selected_elements.length > 0
846 UndoHandler.begin_batch
847 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
848 UndoHandler.end_batch
850 closures[action].call(arg)
852 $selected_elements = {}
855 if optionals.include?('change_image')
856 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
857 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
858 changeimg.signal_connect('activate') { closures[:change].call }
859 menu.append(Gtk::SeparatorMenuItem.new)
861 if !possible_actions[:can_multiple] || $selected_elements.length == 0
864 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
865 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
866 view.signal_connect('activate') { closures[:view].call }
868 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
869 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
870 view.signal_connect('activate') { closures[:view].call }
871 menu.append(Gtk::SeparatorMenuItem.new)
874 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
875 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
876 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
877 exif.signal_connect('activate') { show_popup($main_window,
878 utf8(`exif -m #{fullpath}`),
879 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
880 menu.append(Gtk::SeparatorMenuItem.new)
883 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
884 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
885 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
886 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
887 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
888 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
889 if !possible_actions[:can_multiple] || $selected_elements.length == 0
890 menu.append(Gtk::SeparatorMenuItem.new)
891 if !possible_actions[:forbid_left]
892 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
893 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
894 moveleft.signal_connect('activate') { closures[:move].call('left') }
895 if !possible_actions[:can_left]
896 moveleft.sensitive = false
899 if !possible_actions[:forbid_right]
900 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
901 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
902 moveright.signal_connect('activate') { closures[:move].call('right') }
903 if !possible_actions[:can_right]
904 moveright.sensitive = false
907 if optionals.include?('move_top')
908 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
909 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
910 movetop.signal_connect('activate') { closures[:move].call('top') }
911 if !possible_actions[:can_top]
912 movetop.sensitive = false
915 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
916 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
917 moveup.signal_connect('activate') { closures[:move].call('up') }
918 if !possible_actions[:can_up]
919 moveup.sensitive = false
921 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
922 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
923 movedown.signal_connect('activate') { closures[:move].call('down') }
924 if !possible_actions[:can_down]
925 movedown.sensitive = false
927 if optionals.include?('move_bottom')
928 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
929 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
930 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
931 if !possible_actions[:can_bottom]
932 movebottom.sensitive = false
937 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
938 menu.append(Gtk::SeparatorMenuItem.new)
939 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
940 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
941 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
942 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
943 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
944 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
945 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
946 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
947 seektime.signal_connect('activate') {
948 if possible_actions[:can_multiple] && $selected_elements.length > 0
949 if values = ask_new_seektime(nil, '')
950 distribute_multiple_call.call(:seektime, values)
953 closures[:seektime].call
958 menu.append( Gtk::SeparatorMenuItem.new)
959 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
960 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
961 gammacorrect.signal_connect('activate') {
962 if possible_actions[:can_multiple] && $selected_elements.length > 0
963 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
964 distribute_multiple_call.call(:gammacorrect, values)
967 closures[:gammacorrect].call
970 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
971 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
972 whitebalance.signal_connect('activate') {
973 if possible_actions[:can_multiple] && $selected_elements.length > 0
974 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
975 distribute_multiple_call.call(:whitebalance, values)
978 closures[:whitebalance].call
981 if !possible_actions[:can_multiple] || $selected_elements.length == 0
982 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
983 _("Enhance constrast"))))
985 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
987 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
988 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
989 if type == 'image' && possible_actions[:can_panorama]
990 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
991 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
992 panorama.signal_connect('activate') {
993 if possible_actions[:can_multiple] && $selected_elements.length > 0
994 if values = ask_new_pano_amount(nil, '')
995 distribute_multiple_call.call(:pano, values)
998 distribute_multiple_call.call(:pano)
1002 menu.append( Gtk::SeparatorMenuItem.new)
1003 if optionals.include?('delete')
1004 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1005 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1006 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1007 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1008 paste_item.signal_connect('activate') { closures[:paste].call }
1009 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1010 clear_item.signal_connect('activate') { $cuts = [] }
1012 paste_item.sensitive = clear_item.sensitive = false
1015 menu.append( Gtk::SeparatorMenuItem.new)
1017 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1018 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1019 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1020 editexternally.signal_connect('activate') {
1021 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1026 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1027 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1028 if optionals.include?('delete')
1029 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1030 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1033 menu.popup(nil, nil, event.button, event.time)
1036 def delete_current_subalbum
1038 sel = $albums_tv.selection.selected_rows
1039 $xmldir.elements.each { |e|
1040 if e.name == 'image' || e.name == 'video'
1041 e.add_attribute('deleted', 'true')
1044 #- branch if we have a non deleted subalbum
1045 if $xmldir.child_byname_notattr('dir', 'deleted')
1046 $xmldir.delete_attribute('thumbnails-caption')
1047 $xmldir.delete_attribute('thumbnails-captionfile')
1049 $xmldir.add_attribute('deleted', 'true')
1051 while moveup.parent.name == 'dir'
1052 moveup = moveup.parent
1053 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1054 moveup.add_attribute('deleted', 'true')
1061 save_changes('forced')
1062 populate_subalbums_treeview(false)
1063 $albums_tv.selection.select_path(sel[0])
1069 $current_path = nil #- prevent save_changes from being rerun again
1070 sel = $albums_tv.selection.selected_rows
1071 restore_one = proc { |xmldir|
1072 xmldir.elements.each { |e|
1073 if e.name == 'dir' && e.attributes['deleted']
1076 e.delete_attribute('deleted')
1079 restore_one.call($xmldir)
1080 populate_subalbums_treeview(false)
1081 $albums_tv.selection.select_path(sel[0])
1084 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1087 frame1 = Gtk::Frame.new
1088 fullpath = from_utf8("#{$current_path}/#{filename}")
1090 my_gen_real_thumbnail = proc {
1091 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1095 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1096 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1097 pack_start(img = Gtk::Image.new).
1098 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1099 px, mask = pxb.render_pixmap_and_mask(0)
1100 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1101 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1103 frame1.add(img = Gtk::Image.new)
1106 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1107 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1108 my_gen_real_thumbnail.call
1110 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1113 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1115 tooltips = Gtk::Tooltips.new
1116 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1117 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1119 frame2, textview = create_editzone($autotable_sw, 1, img)
1120 textview.buffer.text = caption
1121 textview.set_justification(Gtk::Justification::CENTER)
1123 vbox = Gtk::VBox.new(false, 5)
1124 vbox.pack_start(evtbox, false, false)
1125 vbox.pack_start(frame2, false, false)
1126 autotable.append(vbox, filename)
1128 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1129 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1131 #- to be able to find widgets by name
1132 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1134 cleanup_all_thumbnails = proc {
1135 #- remove out of sync images
1136 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1137 for sizeobj in $images_size
1138 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
1144 cleanup_all_thumbnails.call
1145 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1147 $xmldir.delete_attribute('already-generated')
1148 my_gen_real_thumbnail.call
1151 rotate_and_cleanup = proc { |angle|
1152 cleanup_all_thumbnails.call
1153 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1156 move = proc { |direction|
1157 do_method = "move_#{direction}"
1158 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1160 done = autotable.method(do_method).call(vbox)
1161 textview.grab_focus #- because if moving, focus is stolen
1165 save_undo(_("move %s") % direction,
1167 autotable.method(undo_method).call(vbox)
1168 textview.grab_focus #- because if moving, focus is stolen
1169 autoscroll_if_needed($autotable_sw, img, textview)
1170 $notebook.set_page(1)
1172 autotable.method(do_method).call(vbox)
1173 textview.grab_focus #- because if moving, focus is stolen
1174 autoscroll_if_needed($autotable_sw, img, textview)
1175 $notebook.set_page(1)
1181 color_swap_and_cleanup = proc {
1182 perform_color_swap_and_cleanup = proc {
1183 cleanup_all_thumbnails.call
1184 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1185 my_gen_real_thumbnail.call
1188 perform_color_swap_and_cleanup.call
1190 save_undo(_("color swap"),
1192 perform_color_swap_and_cleanup.call
1194 autoscroll_if_needed($autotable_sw, img, textview)
1195 $notebook.set_page(1)
1197 perform_color_swap_and_cleanup.call
1199 autoscroll_if_needed($autotable_sw, img, textview)
1200 $notebook.set_page(1)
1205 change_seektime_and_cleanup_real = proc { |values|
1206 perform_change_seektime_and_cleanup = proc { |val|
1207 cleanup_all_thumbnails.call
1208 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1209 my_gen_real_thumbnail.call
1211 perform_change_seektime_and_cleanup.call(values[:new])
1213 save_undo(_("specify seektime"),
1215 perform_change_seektime_and_cleanup.call(values[:old])
1217 autoscroll_if_needed($autotable_sw, img, textview)
1218 $notebook.set_page(1)
1220 perform_change_seektime_and_cleanup.call(values[:new])
1222 autoscroll_if_needed($autotable_sw, img, textview)
1223 $notebook.set_page(1)
1228 change_seektime_and_cleanup = proc {
1229 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1230 change_seektime_and_cleanup_real.call(values)
1234 change_pano_amount_and_cleanup_real = proc { |values|
1235 perform_change_pano_amount_and_cleanup = proc { |val|
1236 cleanup_all_thumbnails.call
1237 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1239 perform_change_pano_amount_and_cleanup.call(values[:new])
1241 save_undo(_("change panorama amount"),
1243 perform_change_pano_amount_and_cleanup.call(values[:old])
1245 autoscroll_if_needed($autotable_sw, img, textview)
1246 $notebook.set_page(1)
1248 perform_change_pano_amount_and_cleanup.call(values[:new])
1250 autoscroll_if_needed($autotable_sw, img, textview)
1251 $notebook.set_page(1)
1256 change_pano_amount_and_cleanup = proc {
1257 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1258 change_pano_amount_and_cleanup_real.call(values)
1262 whitebalance_and_cleanup_real = proc { |values|
1263 perform_change_whitebalance_and_cleanup = proc { |val|
1264 cleanup_all_thumbnails.call
1265 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1266 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1267 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1269 perform_change_whitebalance_and_cleanup.call(values[:new])
1271 save_undo(_("fix white balance"),
1273 perform_change_whitebalance_and_cleanup.call(values[:old])
1275 autoscroll_if_needed($autotable_sw, img, textview)
1276 $notebook.set_page(1)
1278 perform_change_whitebalance_and_cleanup.call(values[:new])
1280 autoscroll_if_needed($autotable_sw, img, textview)
1281 $notebook.set_page(1)
1286 whitebalance_and_cleanup = proc {
1287 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1288 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1289 whitebalance_and_cleanup_real.call(values)
1293 gammacorrect_and_cleanup_real = proc { |values|
1294 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1295 cleanup_all_thumbnails.call
1296 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1297 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1298 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1300 perform_change_gammacorrect_and_cleanup.call(values[:new])
1302 save_undo(_("gamma correction"),
1304 perform_change_gammacorrect_and_cleanup.call(values[:old])
1306 autoscroll_if_needed($autotable_sw, img, textview)
1307 $notebook.set_page(1)
1309 perform_change_gammacorrect_and_cleanup.call(values[:new])
1311 autoscroll_if_needed($autotable_sw, img, textview)
1312 $notebook.set_page(1)
1317 gammacorrect_and_cleanup = Proc.new {
1318 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1319 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1320 gammacorrect_and_cleanup_real.call(values)
1324 enhance_and_cleanup = proc {
1325 perform_enhance_and_cleanup = proc {
1326 cleanup_all_thumbnails.call
1327 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1328 my_gen_real_thumbnail.call
1331 cleanup_all_thumbnails.call
1332 perform_enhance_and_cleanup.call
1334 save_undo(_("enhance"),
1336 perform_enhance_and_cleanup.call
1338 autoscroll_if_needed($autotable_sw, img, textview)
1339 $notebook.set_page(1)
1341 perform_enhance_and_cleanup.call
1343 autoscroll_if_needed($autotable_sw, img, textview)
1344 $notebook.set_page(1)
1349 delete = proc { |isacut|
1350 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 })
1353 perform_delete = proc {
1354 after = autotable.get_next_widget(vbox)
1356 after = autotable.get_previous_widget(vbox)
1358 if $config['deleteondisk'] && !isacut
1359 msg 3, "scheduling for delete: #{fullpath}"
1360 $todelete << fullpath
1362 autotable.remove_widget(vbox)
1364 $vbox2widgets[after][:textview].grab_focus
1365 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1369 previous_pos = autotable.get_current_number(vbox)
1373 delete_current_subalbum
1375 save_undo(_("delete"),
1377 autotable.reinsert(pos, vbox, filename)
1378 $notebook.set_page(1)
1379 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1381 msg 3, "removing deletion schedule of: #{fullpath}"
1382 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1385 $notebook.set_page(1)
1394 $cuts << { :vbox => vbox, :filename => filename }
1395 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1400 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1403 autotable.queue_draws << proc {
1404 $vbox2widgets[last[:vbox]][:textview].grab_focus
1405 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1407 save_undo(_("paste"),
1409 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1410 $notebook.set_page(1)
1413 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1415 $notebook.set_page(1)
1418 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1423 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1424 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1425 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1426 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1428 textview.signal_connect('key-press-event') { |w, event|
1431 x, y = autotable.get_current_pos(vbox)
1432 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1433 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1434 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1435 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1437 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1438 $vbox2widgets[widget_up][:textview].grab_focus
1445 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1447 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1448 $vbox2widgets[widget_down][:textview].grab_focus
1455 if event.keyval == Gdk::Keyval::GDK_Left
1458 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1465 rotate_and_cleanup.call(-90)
1468 if event.keyval == Gdk::Keyval::GDK_Right
1469 next_ = autotable.get_next_widget(vbox)
1470 if next_ && autotable.get_current_pos(next_)[0] > x
1472 $vbox2widgets[next_][:textview].grab_focus
1479 rotate_and_cleanup.call(90)
1482 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1485 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1486 view_element(filename, { :delete => delete })
1489 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1492 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1496 !propagate #- propagate if needed
1499 $ignore_next_release = false
1500 evtbox.signal_connect('button-press-event') { |w, event|
1501 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1502 if event.state & Gdk::Window::BUTTON3_MASK != 0
1503 #- gesture redo: hold right mouse button then click left mouse button
1504 $config['nogestures'] or perform_redo
1505 $ignore_next_release = true
1507 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1509 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1511 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1512 elsif $enhance.active?
1513 enhance_and_cleanup.call
1514 elsif $delete.active?
1518 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1521 $button1_pressed_autotable = true
1522 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1523 if event.state & Gdk::Window::BUTTON1_MASK != 0
1524 #- gesture undo: hold left mouse button then click right mouse button
1525 $config['nogestures'] or perform_undo
1526 $ignore_next_release = true
1528 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1529 view_element(filename, { :delete => delete })
1534 evtbox.signal_connect('button-release-event') { |w, event|
1535 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1536 if !$ignore_next_release
1537 x, y = autotable.get_current_pos(vbox)
1538 next_ = autotable.get_next_widget(vbox)
1539 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1540 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1541 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1542 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1543 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1544 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1545 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1547 $ignore_next_release = false
1548 $gesture_press = nil
1553 #- handle reordering with drag and drop
1554 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1555 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1556 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1557 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1560 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1562 #- mouse gesture first (dnd disables button-release-event)
1563 if $gesture_press && $gesture_press[:filename] == filename
1564 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1565 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1566 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1567 rotate_and_cleanup.call(angle)
1568 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1570 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1571 msg 3, "gesture delete: click-drag right button to the bottom"
1573 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1578 ctxt.targets.each { |target|
1579 if target.name == 'reorder-elements'
1580 move_dnd = proc { |from,to|
1583 autotable.move(from, to)
1584 save_undo(_("reorder"),
1587 autotable.move(to - 1, from)
1589 autotable.move(to, from + 1)
1591 $notebook.set_page(1)
1593 autotable.move(from, to)
1594 $notebook.set_page(1)
1599 if $multiple_dnd.size == 0
1600 move_dnd.call(selection_data.data.to_i,
1601 autotable.get_current_number(vbox))
1603 UndoHandler.begin_batch
1604 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1606 #- need to update current position between each call
1607 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1608 autotable.get_current_number(vbox))
1610 UndoHandler.end_batch
1621 def create_auto_table
1623 $autotable = Gtk::AutoTable.new(5)
1625 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1626 thumbnails_vb = Gtk::VBox.new(false, 5)
1628 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1629 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1630 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1631 thumbnails_vb.add($autotable)
1633 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1634 $autotable_sw.add_with_viewport(thumbnails_vb)
1636 #- follows stuff for handling multiple elements selection
1637 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1639 update_selected = proc {
1640 $autotable.current_order.each { |path|
1641 w = $name2widgets[path][:evtbox].window
1642 xm = w.position[0] + w.size[0]/2
1643 ym = w.position[1] + w.size[1]/2
1644 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1645 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1646 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1647 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1650 if $selected_elements[path] && ! $selected_elements[path][:keep]
1651 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))
1652 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1653 $selected_elements.delete(path)
1658 $autotable.signal_connect('realize') { |w,e|
1659 gc = Gdk::GC.new($autotable.window)
1660 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1661 gc.function = Gdk::GC::INVERT
1662 #- autoscroll handling for DND and multiple selections
1663 Gtk.timeout_add(100) {
1664 if ! $autotable.window.nil?
1665 w, x, y, mask = $autotable.window.pointer
1666 if mask & Gdk::Window::BUTTON1_MASK != 0
1667 if y < $autotable_sw.vadjustment.value
1669 $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]])
1671 if $button1_pressed_autotable || press_x
1672 scroll_upper($autotable_sw, y)
1675 w, pos_x, pos_y = $autotable.window.pointer
1676 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1677 update_selected.call
1680 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1682 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1684 if $button1_pressed_autotable || press_x
1685 scroll_lower($autotable_sw, y)
1688 w, pos_x, pos_y = $autotable.window.pointer
1689 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1690 update_selected.call
1695 ! $autotable.window.nil?
1699 $autotable.signal_connect('button-press-event') { |w,e|
1701 if !$button1_pressed_autotable
1704 if e.state & Gdk::Window::SHIFT_MASK == 0
1705 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1706 $selected_elements = {}
1707 $statusbar.push(0, utf8(_("Nothing selected.")))
1709 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1711 set_mousecursor(Gdk::Cursor::TCROSS)
1715 $autotable.signal_connect('button-release-event') { |w,e|
1717 if $button1_pressed_autotable
1718 #- unselect all only now
1719 $multiple_dnd = $selected_elements.keys
1720 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1721 $selected_elements = {}
1722 $button1_pressed_autotable = false
1725 $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]])
1726 if $selected_elements.length > 0
1727 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1730 press_x = press_y = pos_x = pos_y = nil
1731 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1735 $autotable.signal_connect('motion-notify-event') { |w,e|
1738 $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]])
1742 $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]])
1743 update_selected.call
1749 def create_subalbums_page
1751 subalbums_hb = Gtk::HBox.new
1752 $subalbums_vb = Gtk::VBox.new(false, 5)
1753 subalbums_hb.pack_start($subalbums_vb, false, false)
1754 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1755 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1756 $subalbums_sw.add_with_viewport(subalbums_hb)
1759 def save_current_file
1765 ios = File.open($filename, "w")
1766 $xmldoc.write(ios, 0)
1768 rescue Iconv::IllegalSequence
1769 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1770 if ! ios.nil? && ! ios.closed?
1773 $xmldoc.xml_decl.encoding = 'UTF-8'
1774 ios = File.open($filename, "w")
1775 $xmldoc.write(ios, 0)
1786 def save_current_file_user
1787 save_tempfilename = $filename
1788 $filename = $orig_filename
1789 if ! save_current_file
1790 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1791 $filename = save_tempfilename
1795 $generated_outofline = false
1796 $filename = save_tempfilename
1798 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1799 $todelete.each { |f|
1800 system("rm -f #{f}")
1804 def mark_document_as_dirty
1805 $xmldoc.elements.each('//dir') { |elem|
1806 elem.delete_attribute('already-generated')
1810 #- ret: true => ok false => cancel
1811 def ask_save_modifications(msg1, msg2, *options)
1813 options = options.size > 0 ? options[0] : {}
1815 if options[:disallow_cancel]
1816 dialog = Gtk::Dialog.new(msg1,
1818 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1819 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1820 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1822 dialog = Gtk::Dialog.new(msg1,
1824 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1825 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1826 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1827 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1829 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1830 dialog.vbox.add(Gtk::Label.new(msg2))
1831 dialog.window_position = Gtk::Window::POS_CENTER
1834 dialog.run { |response|
1836 if response == Gtk::Dialog::RESPONSE_YES
1837 if ! save_current_file_user
1838 return ask_save_modifications(msg1, msg2, options)
1841 #- if we have generated an album but won't save modifications, we must remove
1842 #- already-generated markers in original file
1843 if $generated_outofline
1845 $xmldoc = REXML::Document.new File.new($orig_filename)
1846 mark_document_as_dirty
1847 ios = File.open($orig_filename, "w")
1848 $xmldoc.write(ios, 0)
1851 puts "exception: #{$!}"
1855 if response == Gtk::Dialog::RESPONSE_CANCEL
1858 $todelete = [] #- unconditionally clear the list of images/videos to delete
1864 def try_quit(*options)
1865 if ask_save_modifications(utf8(_("Save before quitting?")),
1866 utf8(_("Do you want to save your changes before quitting?")),
1872 def show_popup(parent, msg, *options)
1873 dialog = Gtk::Dialog.new
1874 if options[0] && options[0][:title]
1875 dialog.title = options[0][:title]
1877 dialog.title = utf8(_("Booh message"))
1879 lbl = Gtk::Label.new
1880 if options[0] && options[0][:nomarkup]
1885 if options[0] && options[0][:centered]
1886 lbl.set_justify(Gtk::Justification::CENTER)
1888 if options[0] && options[0][:selectable]
1889 lbl.selectable = true
1891 if options[0] && options[0][:topwidget]
1892 dialog.vbox.add(options[0][:topwidget])
1894 if options[0] && options[0][:scrolled]
1895 sw = Gtk::ScrolledWindow.new(nil, nil)
1896 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1897 sw.add_with_viewport(lbl)
1899 dialog.set_default_size(500, 600)
1901 dialog.vbox.add(lbl)
1902 dialog.set_default_size(200, 120)
1904 if options[0] && options[0][:okcancel]
1905 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1907 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1909 if options[0] && options[0][:pos_centered]
1910 dialog.window_position = Gtk::Window::POS_CENTER
1912 dialog.window_position = Gtk::Window::POS_MOUSE
1915 if options[0] && options[0][:linkurl]
1916 linkbut = Gtk::Button.new('')
1917 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1918 linkbut.signal_connect('clicked') {
1919 open_url(options[0][:linkurl] + '/index.html')
1920 dialog.response(Gtk::Dialog::RESPONSE_OK)
1921 set_mousecursor_normal
1923 linkbut.relief = Gtk::RELIEF_NONE
1924 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1925 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1926 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1931 if !options[0] || !options[0][:not_transient]
1932 dialog.transient_for = parent
1933 dialog.run { |response|
1935 if options[0] && options[0][:okcancel]
1936 return response == Gtk::Dialog::RESPONSE_OK
1940 dialog.signal_connect('response') { dialog.destroy }
1944 def backend_wait_message(parent, msg, infopipe_path, mode)
1946 w.set_transient_for(parent)
1949 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1950 vb.pack_start(Gtk::Label.new(msg), false, false)
1952 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1953 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1954 if mode != 'one dir scan'
1955 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1957 if mode == 'web-album'
1958 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1959 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1961 vb.pack_start(Gtk::HSeparator.new, false, false)
1963 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1964 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1965 vb.pack_end(bottom, false, false)
1967 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1968 refresh_thread = Thread.new {
1969 directories_counter = 0
1970 while line = infopipe.gets
1971 if line =~ /^directories: (\d+), sizes: (\d+)/
1972 directories = $1.to_f + 1
1974 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1975 elements = $3.to_f + 1
1976 if mode == 'web-album'
1980 gtk_thread_protect { pb1_1.fraction = 0 }
1981 if mode != 'one dir scan'
1982 newtext = utf8(full_src_dir_to_rel($1, $2))
1983 newtext = '/' if newtext == ''
1984 gtk_thread_protect { pb1_2.text = newtext }
1985 directories_counter += 1
1986 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1988 elsif line =~ /^processing element$/
1989 element_counter += 1
1990 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1991 elsif line =~ /^processing size$/
1992 element_counter += 1
1993 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1994 elsif line =~ /^finished processing sizes$/
1995 gtk_thread_protect { pb1_1.fraction = 1 }
1996 elsif line =~ /^creating index.html$/
1997 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1998 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1999 directories_counter = 0
2000 elsif line =~ /^index.html: (.+)\|(.+)/
2001 newtext = utf8(full_src_dir_to_rel($1, $2))
2002 newtext = '/' if newtext == ''
2003 gtk_thread_protect { pb2.text = newtext }
2004 directories_counter += 1
2005 gtk_thread_protect { pb2.fraction = directories_counter / directories }
2006 elsif line =~ /^die: (.*)$/
2013 w.signal_connect('delete-event') { w.destroy }
2014 w.signal_connect('destroy') {
2015 Thread.kill(refresh_thread)
2016 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2019 system("rm -f #{infopipe_path}")
2022 w.window_position = Gtk::Window::POS_CENTER
2028 def call_backend(cmd, waitmsg, mode, params)
2029 pipe = Tempfile.new("boohpipe")
2031 system("mkfifo #{pipe.path}")
2032 cmd += " --info-pipe #{pipe.path}"
2033 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2038 id, exitstatus = Process.waitpid2(pid)
2039 gtk_thread_protect { w8.destroy }
2041 if params[:successmsg]
2042 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2044 if params[:closure_after]
2045 gtk_thread_protect(¶ms[:closure_after])
2047 elsif exitstatus == 15
2048 #- say nothing, user aborted
2050 gtk_thread_protect { show_popup($main_window,
2051 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2057 button.signal_connect('clicked') {
2058 Process.kill('SIGTERM', pid)
2062 def save_changes(*forced)
2063 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2067 $xmldir.delete_attribute('already-generated')
2069 propagate_children = proc { |xmldir|
2070 if xmldir.attributes['subdirs-caption']
2071 xmldir.delete_attribute('already-generated')
2073 xmldir.elements.each('dir') { |element|
2074 propagate_children.call(element)
2078 if $xmldir.child_byname_notattr('dir', 'deleted')
2079 new_title = $subalbums_title.buffer.text
2080 if new_title != $xmldir.attributes['subdirs-caption']
2081 parent = $xmldir.parent
2082 if parent.name == 'dir'
2083 parent.delete_attribute('already-generated')
2085 propagate_children.call($xmldir)
2087 $xmldir.add_attribute('subdirs-caption', new_title)
2088 $xmldir.elements.each('dir') { |element|
2089 if !element.attributes['deleted']
2090 path = element.attributes['path']
2091 newtext = $subalbums_edits[path][:editzone].buffer.text
2092 if element.attributes['subdirs-caption']
2093 if element.attributes['subdirs-caption'] != newtext
2094 propagate_children.call(element)
2096 element.add_attribute('subdirs-caption', newtext)
2097 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2099 if element.attributes['thumbnails-caption'] != newtext
2100 element.delete_attribute('already-generated')
2102 element.add_attribute('thumbnails-caption', newtext)
2103 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2109 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2110 if $xmldir.attributes['thumbnails-caption']
2111 path = $xmldir.attributes['path']
2112 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2114 elsif $xmldir.attributes['thumbnails-caption']
2115 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2118 if $xmldir.attributes['thumbnails-caption']
2119 if edit = $subalbums_edits[$xmldir.attributes['path']]
2120 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2124 #- remove and reinsert elements to reflect new ordering
2127 $xmldir.elements.each { |element|
2128 if element.name == 'image' || element.name == 'video'
2129 saves[element.attributes['filename']] = element.remove
2133 $autotable.current_order.each { |path|
2134 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2135 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2138 saves.each_key { |path|
2139 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2140 chld.add_attribute('deleted', 'true')
2144 def sort_by_exif_date
2148 $xmldir.elements.each { |element|
2149 if element.name == 'image' || element.name == 'video'
2150 current_order << element.attributes['filename']
2154 #- look for EXIF dates
2157 if current_order.size > 20
2159 w.set_transient_for($main_window)
2161 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2162 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2163 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2164 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2165 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2166 vb.pack_end(bottom, false, false)
2168 w.signal_connect('delete-event') { w.destroy }
2169 w.window_position = Gtk::Window::POS_CENTER
2173 b.signal_connect('clicked') { aborted = true }
2175 current_order.each { |f|
2177 if entry2type(f) == 'image'
2179 pb.fraction = i.to_f / current_order.size
2180 Gtk.main_iteration while Gtk.events_pending?
2181 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2183 dates[f] = date_time
2196 current_order.each { |f|
2197 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2199 dates[f] = date_time
2205 $xmldir.elements.each { |element|
2206 if element.name == 'image' || element.name == 'video'
2207 saves[element.attributes['filename']] = element.remove
2211 neworder = smartsort(current_order, dates)
2214 $xmldir.add_element(saves[f].name, saves[f].attributes)
2217 #- let the auto-table reflect new ordering
2221 def remove_all_captions
2224 $autotable.current_order.each { |path|
2225 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2226 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2228 save_undo(_("remove all captions"),
2230 texts.each_key { |key|
2231 $name2widgets[key][:textview].buffer.text = texts[key]
2233 $notebook.set_page(1)
2235 texts.each_key { |key|
2236 $name2widgets[key][:textview].buffer.text = ''
2238 $notebook.set_page(1)
2244 $selected_elements.each_key { |path|
2245 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2251 $selected_elements = {}
2255 $undo_tb.sensitive = $undo_mb.sensitive = false
2256 $redo_tb.sensitive = $redo_mb.sensitive = false
2262 $subalbums_vb.children.each { |chld|
2263 $subalbums_vb.remove(chld)
2265 $subalbums = Gtk::Table.new(0, 0, true)
2266 current_y_sub_albums = 0
2268 $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"])
2269 $subalbums_edits = {}
2270 subalbums_counter = 0
2271 subalbums_edits_bypos = {}
2273 add_subalbum = proc { |xmldir, counter|
2274 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2275 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2276 if xmldir == $xmldir
2277 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2278 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2279 caption = xmldir.attributes['thumbnails-caption']
2280 infotype = 'thumbnails'
2282 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2283 captionfile, caption = find_subalbum_caption_info(xmldir)
2284 infotype = find_subalbum_info_type(xmldir)
2286 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2287 hbox = Gtk::HBox.new
2288 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2290 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2293 my_gen_real_thumbnail = proc {
2294 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2297 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2298 f.add(img = Gtk::Image.new)
2299 my_gen_real_thumbnail.call
2301 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2303 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2304 $subalbums.attach(hbox,
2305 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2307 frame, textview = create_editzone($subalbums_sw, 0, img)
2308 textview.buffer.text = caption
2309 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2310 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2312 change_image = proc {
2313 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2315 Gtk::FileChooser::ACTION_OPEN,
2317 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2318 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2319 fc.transient_for = $main_window
2320 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))
2321 f.add(preview_img = Gtk::Image.new)
2323 fc.signal_connect('update-preview') { |w|
2325 if fc.preview_filename
2326 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2327 fc.preview_widget_active = true
2329 rescue Gdk::PixbufError
2330 fc.preview_widget_active = false
2333 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2335 old_file = captionfile
2336 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2337 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2338 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2339 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2341 new_file = fc.filename
2342 msg 3, "new captionfile is: #{fc.filename}"
2343 perform_changefile = proc {
2344 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2345 $modified_pixbufs.delete(thumbnail_file)
2346 xmldir.delete_attribute("#{infotype}-rotate")
2347 xmldir.delete_attribute("#{infotype}-color-swap")
2348 xmldir.delete_attribute("#{infotype}-enhance")
2349 xmldir.delete_attribute("#{infotype}-seektime")
2350 my_gen_real_thumbnail.call
2352 perform_changefile.call
2354 save_undo(_("change caption file for sub-album"),
2356 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2357 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2358 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2359 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2360 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2361 my_gen_real_thumbnail.call
2362 $notebook.set_page(0)
2364 perform_changefile.call
2365 $notebook.set_page(0)
2373 system("rm -f '#{thumbnail_file}'")
2374 my_gen_real_thumbnail.call
2377 rotate_and_cleanup = proc { |angle|
2378 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2379 system("rm -f '#{thumbnail_file}'")
2382 move = proc { |direction|
2385 save_changes('forced')
2386 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2387 if direction == 'up'
2388 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2389 subalbums_edits_bypos[oldpos - 1][:position] += 1
2391 if direction == 'down'
2392 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2393 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2395 if direction == 'top'
2396 for i in 1 .. oldpos - 1
2397 subalbums_edits_bypos[i][:position] += 1
2399 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2401 if direction == 'bottom'
2402 for i in oldpos + 1 .. subalbums_counter
2403 subalbums_edits_bypos[i][:position] -= 1
2405 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2409 $xmldir.elements.each('dir') { |element|
2410 if (!element.attributes['deleted'])
2411 elems << [ element.attributes['path'], element.remove ]
2414 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2415 each { |e| $xmldir.add_element(e[1]) }
2416 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2417 $xmldir.elements.each('descendant::dir') { |elem|
2418 elem.delete_attribute('already-generated')
2421 sel = $albums_tv.selection.selected_rows
2423 populate_subalbums_treeview(false)
2424 $albums_tv.selection.select_path(sel[0])
2427 color_swap_and_cleanup = proc {
2428 perform_color_swap_and_cleanup = proc {
2429 color_swap(xmldir, "#{infotype}-")
2430 my_gen_real_thumbnail.call
2432 perform_color_swap_and_cleanup.call
2434 save_undo(_("color swap"),
2436 perform_color_swap_and_cleanup.call
2437 $notebook.set_page(0)
2439 perform_color_swap_and_cleanup.call
2440 $notebook.set_page(0)
2445 change_seektime_and_cleanup = proc {
2446 if values = ask_new_seektime(xmldir, "#{infotype}-")
2447 perform_change_seektime_and_cleanup = proc { |val|
2448 change_seektime(xmldir, "#{infotype}-", val)
2449 my_gen_real_thumbnail.call
2451 perform_change_seektime_and_cleanup.call(values[:new])
2453 save_undo(_("specify seektime"),
2455 perform_change_seektime_and_cleanup.call(values[:old])
2456 $notebook.set_page(0)
2458 perform_change_seektime_and_cleanup.call(values[:new])
2459 $notebook.set_page(0)
2465 whitebalance_and_cleanup = proc {
2466 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2467 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2468 perform_change_whitebalance_and_cleanup = proc { |val|
2469 change_whitebalance(xmldir, "#{infotype}-", val)
2470 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2471 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2472 system("rm -f '#{thumbnail_file}'")
2474 perform_change_whitebalance_and_cleanup.call(values[:new])
2476 save_undo(_("fix white balance"),
2478 perform_change_whitebalance_and_cleanup.call(values[:old])
2479 $notebook.set_page(0)
2481 perform_change_whitebalance_and_cleanup.call(values[:new])
2482 $notebook.set_page(0)
2488 gammacorrect_and_cleanup = proc {
2489 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2490 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2491 perform_change_gammacorrect_and_cleanup = proc { |val|
2492 change_gammacorrect(xmldir, "#{infotype}-", val)
2493 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2494 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2495 system("rm -f '#{thumbnail_file}'")
2497 perform_change_gammacorrect_and_cleanup.call(values[:new])
2499 save_undo(_("gamma correction"),
2501 perform_change_gammacorrect_and_cleanup.call(values[:old])
2502 $notebook.set_page(0)
2504 perform_change_gammacorrect_and_cleanup.call(values[:new])
2505 $notebook.set_page(0)
2511 enhance_and_cleanup = proc {
2512 perform_enhance_and_cleanup = proc {
2513 enhance(xmldir, "#{infotype}-")
2514 my_gen_real_thumbnail.call
2517 perform_enhance_and_cleanup.call
2519 save_undo(_("enhance"),
2521 perform_enhance_and_cleanup.call
2522 $notebook.set_page(0)
2524 perform_enhance_and_cleanup.call
2525 $notebook.set_page(0)
2530 evtbox.signal_connect('button-press-event') { |w, event|
2531 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2533 rotate_and_cleanup.call(90)
2535 rotate_and_cleanup.call(-90)
2536 elsif $enhance.active?
2537 enhance_and_cleanup.call
2540 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2541 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2542 { :forbid_left => true, :forbid_right => true,
2543 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2544 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2545 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2546 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2547 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2549 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2554 evtbox.signal_connect('button-press-event') { |w, event|
2555 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2559 evtbox.signal_connect('button-release-event') { |w, event|
2560 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2561 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2562 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2563 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2564 msg 3, "gesture rotate: #{angle}"
2565 rotate_and_cleanup.call(angle)
2568 $gesture_press = nil
2571 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2572 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2573 current_y_sub_albums += 1
2576 if $xmldir.child_byname_notattr('dir', 'deleted')
2578 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2579 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2580 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2581 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2582 #- this album image/caption
2583 if $xmldir.attributes['thumbnails-caption']
2584 add_subalbum.call($xmldir, 0)
2587 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2588 $xmldir.elements.each { |element|
2589 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2590 #- element (image or video) of this album
2591 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2592 msg 3, "dest_img: #{dest_img}"
2593 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2594 total[element.name] += 1
2596 if element.name == 'dir' && !element.attributes['deleted']
2597 #- sub-album image/caption
2598 add_subalbum.call(element, subalbums_counter += 1)
2599 total[element.name] += 1
2602 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2603 total['image'], total['video'], total['dir'] ]))
2604 $subalbums_vb.add($subalbums)
2605 $subalbums_vb.show_all
2607 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2608 $notebook.get_tab_label($autotable_sw).sensitive = false
2609 $notebook.set_page(0)
2610 $thumbnails_title.buffer.text = ''
2612 $notebook.get_tab_label($autotable_sw).sensitive = true
2613 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2616 if !$xmldir.child_byname_notattr('dir', 'deleted')
2617 $notebook.get_tab_label($subalbums_sw).sensitive = false
2618 $notebook.set_page(1)
2620 $notebook.get_tab_label($subalbums_sw).sensitive = true
2624 def pixbuf_or_nil(filename)
2626 return Gdk::Pixbuf.new(filename)
2632 def theme_choose(current)
2633 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2635 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2636 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2637 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2639 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2640 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2641 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2642 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2643 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2644 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2645 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2646 treeview.signal_connect('button-press-event') { |w, event|
2647 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2648 dialog.response(Gtk::Dialog::RESPONSE_OK)
2652 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2654 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2657 iter[0] = File.basename(dir)
2658 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2659 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2660 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2661 if File.basename(dir) == current
2662 treeview.selection.select_iter(iter)
2666 dialog.set_default_size(700, 400)
2667 dialog.vbox.show_all
2668 dialog.run { |response|
2669 iter = treeview.selection.selected
2671 if response == Gtk::Dialog::RESPONSE_OK && iter
2672 return model.get_value(iter, 0)
2678 def show_password_protections
2679 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2680 child_iter = $albums_iters[xmldir.attributes['path']]
2681 if xmldir.attributes['password-protect']
2682 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2683 already_protected = true
2684 elsif already_protected
2685 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2687 pix = pix.saturate_and_pixelate(1, true)
2693 xmldir.elements.each('dir') { |elem|
2694 if !elem.attributes['deleted']
2695 examine_dir_elem.call(child_iter, elem, already_protected)
2699 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2702 def populate_subalbums_treeview(select_first)
2706 $subalbums_vb.children.each { |chld|
2707 $subalbums_vb.remove(chld)
2710 source = $xmldoc.root.attributes['source']
2711 msg 3, "source: #{source}"
2713 xmldir = $xmldoc.elements['//dir']
2714 if !xmldir || xmldir.attributes['path'] != source
2715 msg 1, _("Corrupted booh file...")
2719 append_dir_elem = proc { |parent_iter, xmldir|
2720 child_iter = $albums_ts.append(parent_iter)
2721 child_iter[0] = File.basename(xmldir.attributes['path'])
2722 child_iter[1] = xmldir.attributes['path']
2723 $albums_iters[xmldir.attributes['path']] = child_iter
2724 msg 3, "puttin location: #{xmldir.attributes['path']}"
2725 xmldir.elements.each('dir') { |elem|
2726 if !elem.attributes['deleted']
2727 append_dir_elem.call(child_iter, elem)
2731 append_dir_elem.call(nil, xmldir)
2732 show_password_protections
2734 $albums_tv.expand_all
2736 $albums_tv.selection.select_iter($albums_ts.iter_first)
2740 def select_current_theme
2741 select_theme($xmldoc.root.attributes['theme'],
2742 $xmldoc.root.attributes['limit-sizes'],
2743 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2744 $xmldoc.root.attributes['thumbnails-per-row'])
2747 def open_file(filename)
2751 $current_path = nil #- invalidate
2752 $modified_pixbufs = {}
2755 $subalbums_vb.children.each { |chld|
2756 $subalbums_vb.remove(chld)
2759 if !File.exists?(filename)
2760 return utf8(_("File not found."))
2764 $xmldoc = REXML::Document.new File.new(filename)
2769 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2770 if entry2type(filename).nil?
2771 return utf8(_("Not a booh file!"))
2773 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."))
2777 if !source = $xmldoc.root.attributes['source']
2778 return utf8(_("Corrupted booh file..."))
2781 if !dest = $xmldoc.root.attributes['destination']
2782 return utf8(_("Corrupted booh file..."))
2785 if !theme = $xmldoc.root.attributes['theme']
2786 return utf8(_("Corrupted booh file..."))
2789 if $xmldoc.root.attributes['version'] < '0.8.6'
2790 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2791 mark_document_as_dirty
2792 if $xmldoc.root.attributes['version'] < '0.8.4'
2793 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2794 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2795 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2796 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2797 if old_dest_dir != new_dest_dir
2798 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2800 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2801 xmldir.elements.each { |element|
2802 if %w(image video).include?(element.name) && !element.attributes['deleted']
2803 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2804 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2805 Dir[old_name + '*'].each { |file|
2806 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2807 file != new_file and sys("mv '#{file}' '#{new_file}'")
2810 if element.name == 'dir' && !element.attributes['deleted']
2811 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2812 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2813 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2817 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2821 $xmldoc.root.add_attribute('version', $VERSION)
2824 select_current_theme
2826 $filename = filename
2827 $default_size['thumbnails'] =~ /(.*)x(.*)/
2828 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2829 $albums_thumbnail_size =~ /(.*)x(.*)/
2830 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2832 populate_subalbums_treeview(true)
2834 $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
2838 def open_file_user(filename)
2839 result = open_file(filename)
2841 $config['last-opens'] ||= []
2842 if $config['last-opens'][-1] != utf8(filename)
2843 $config['last-opens'] << utf8(filename)
2845 $orig_filename = $filename
2846 tmp = Tempfile.new("boohtemp")
2849 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2851 $tempfiles << $filename << "#{$filename}.backup"
2853 $orig_filename = nil
2859 if !ask_save_modifications(utf8(_("Save this album?")),
2860 utf8(_("Do you want to save the changes to this album?")),
2861 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2864 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2866 Gtk::FileChooser::ACTION_OPEN,
2868 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2869 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2870 fc.set_current_folder(File.expand_path("~/.booh"))
2871 fc.transient_for = $main_window
2874 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2875 push_mousecursor_wait(fc)
2876 msg = open_file_user(fc.filename)
2891 def additional_booh_options
2894 options += "--mproc #{$config['mproc'].to_i} "
2896 options += "--comments-format '#{$config['comments-format']}'"
2901 if !ask_save_modifications(utf8(_("Save this album?")),
2902 utf8(_("Do you want to save the changes to this album?")),
2903 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2906 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2908 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2909 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2910 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2912 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2913 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2914 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2915 tbl.attach(src = Gtk::Entry.new,
2916 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2917 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2918 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2919 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2920 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2921 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2922 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2923 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2924 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2925 tbl.attach(dest = Gtk::Entry.new,
2926 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2927 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2928 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2929 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2930 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2931 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2932 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2933 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2934 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2936 tooltips = Gtk::Tooltips.new
2937 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2938 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2939 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2940 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2941 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2942 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2943 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)
2944 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2945 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2946 nperpage_model = Gtk::ListStore.new(String, String)
2947 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
2948 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
2949 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
2950 nperpagecombo.set_attributes(crt, { :markup => 0 })
2951 iter = nperpage_model.append
2952 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
2954 [ 12, 20, 30, 40, 50 ].each { |v|
2955 iter = nperpage_model.append
2956 iter[0] = iter[1] = v.to_s
2958 nperpagecombo.active = 0
2959 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
2960 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
2961 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)
2962 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2963 pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
2964 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)
2966 src_nb_calculated_for = ''
2968 process_src_nb = proc {
2969 if src.text != src_nb_calculated_for
2970 src_nb_calculated_for = src.text
2972 Thread.kill(src_nb_thread)
2975 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
2976 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
2978 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2979 if File.readable?(from_utf8_safe(src_nb_calculated_for))
2980 src_nb_thread = Thread.new {
2981 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2982 total = { 'image' => 0, 'video' => 0, nil => 0 }
2983 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2984 if File.basename(dir) =~ /^\./
2988 Dir.entries(dir.chomp).each { |file|
2989 total[entry2type(file)] += 1
2991 rescue Errno::EACCES, Errno::ENOENT
2995 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2999 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3002 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3008 timeout_src_nb = Gtk.timeout_add(100) {
3012 src_browse.signal_connect('clicked') {
3013 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
3015 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3017 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3018 fc.transient_for = $main_window
3019 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3020 src.text = utf8(fc.filename)
3022 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3027 dest_browse.signal_connect('clicked') {
3028 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3030 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3032 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3033 fc.transient_for = $main_window
3034 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3035 dest.text = utf8(fc.filename)
3040 conf_browse.signal_connect('clicked') {
3041 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3043 Gtk::FileChooser::ACTION_SAVE,
3045 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3046 fc.transient_for = $main_window
3047 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3048 fc.set_current_folder(File.expand_path("~/.booh"))
3049 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3050 conf.text = utf8(fc.filename)
3057 recreate_theme_config = proc {
3058 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3060 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3061 $images_size.each { |s|
3062 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3066 tooltips.set_tip(cb, utf8(s['description']), nil)
3067 theme_sizes << { :widget => cb, :value => s['name'] }
3069 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3070 tooltips = Gtk::Tooltips.new
3071 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3072 theme_sizes << { :widget => cb, :value => 'original' }
3075 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3078 $allowed_N_values.each { |n|
3080 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3082 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3084 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3088 nperrows << { :widget => rb, :value => n }
3090 nperrowradios.show_all
3092 recreate_theme_config.call
3094 theme_button.signal_connect('clicked') {
3095 if newtheme = theme_choose(theme_button.label)
3096 theme_button.label = newtheme
3097 recreate_theme_config.call
3101 dialog.vbox.add(frame1)
3102 dialog.vbox.add(frame2)
3108 dialog.run { |response|
3109 if response == Gtk::Dialog::RESPONSE_OK
3110 srcdir = from_utf8_safe(src.text)
3111 destdir = from_utf8_safe(dest.text)
3112 confpath = from_utf8_safe(conf.text)
3113 if src.text != '' && srcdir == ''
3114 show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3116 elsif !File.directory?(srcdir)
3117 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3119 elsif dest.text != '' && destdir == ''
3120 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3122 elsif destdir != make_dest_filename(destdir)
3123 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3125 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3126 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
3128 elsif File.exists?(destdir) && !File.directory?(destdir)
3129 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3131 elsif conf.text == ''
3132 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3134 elsif conf.text != '' && confpath == ''
3135 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3137 elsif File.directory?(confpath)
3138 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3140 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3141 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3143 system("mkdir '#{destdir}'")
3144 if !File.directory?(destdir)
3145 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3157 srcdir = from_utf8(src.text)
3158 destdir = from_utf8(dest.text)
3159 configskel = File.expand_path(from_utf8(conf.text))
3160 theme = theme_button.label
3161 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3162 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3163 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3164 opt432 = optimize432.active?
3165 madewith = madewithentry.text.gsub('"', '"').gsub('\'', ''')
3166 indexlink = indexlinkentry.text.gsub('"', '"').gsub('\'', ''')
3169 Thread.kill(src_nb_thread)
3170 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3173 Gtk.timeout_remove(timeout_src_nb)
3176 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3177 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3178 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3179 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3180 utf8(_("Please wait while scanning source directory...")),
3182 { :closure_after => proc { open_file_user(configskel) } })
3187 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3189 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3190 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3191 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3193 source = $xmldoc.root.attributes['source']
3194 dest = $xmldoc.root.attributes['destination']
3195 theme = $xmldoc.root.attributes['theme']
3196 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3197 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3198 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3199 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3201 limit_sizes = limit_sizes.split(/,/)
3203 madewith = $xmldoc.root.attributes['made-with'].gsub(''', '\'')
3204 indexlink = $xmldoc.root.attributes['index-link'].gsub(''', '\'')
3206 tooltips = Gtk::Tooltips.new
3207 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3208 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3209 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3210 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3211 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3212 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3213 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3214 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3215 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3216 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3217 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3218 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3219 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3221 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3222 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3223 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3224 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3225 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3226 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3227 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)
3228 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3229 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3230 nperpage_model = Gtk::ListStore.new(String, String)
3231 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3232 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3233 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3234 nperpagecombo.set_attributes(crt, { :markup => 0 })
3235 iter = nperpage_model.append
3236 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3238 [ 12, 20, 30, 40, 50 ].each { |v|
3239 iter = nperpage_model.append
3240 iter[0] = iter[1] = v.to_s
3241 if nperpage && nperpage == v.to_s
3242 nperpagecombo.active_iter = iter
3245 if nperpagecombo.active_iter.nil?
3246 nperpagecombo.active = 0
3249 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3250 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3252 indexlinkentry.text = indexlink
3254 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)
3255 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3256 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3258 madewithentry.text = madewith
3260 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)
3264 recreate_theme_config = proc {
3265 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3267 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3269 $images_size.each { |s|
3270 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3272 if limit_sizes.include?(s['name'])
3280 tooltips.set_tip(cb, utf8(s['description']), nil)
3281 theme_sizes << { :widget => cb, :value => s['name'] }
3283 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3284 tooltips = Gtk::Tooltips.new
3285 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3286 if limit_sizes && limit_sizes.include?('original')
3289 theme_sizes << { :widget => cb, :value => 'original' }
3292 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3295 $allowed_N_values.each { |n|
3297 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3299 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3301 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3302 nperrowradios.add(Gtk::Label.new(' '))
3303 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3306 nperrows << { :widget => rb, :value => n.to_s }
3308 nperrowradios.show_all
3310 recreate_theme_config.call
3312 theme_button.signal_connect('clicked') {
3313 if newtheme = theme_choose(theme_button.label)
3316 theme_button.label = newtheme
3317 recreate_theme_config.call
3321 dialog.vbox.add(frame1)
3322 dialog.vbox.add(frame2)
3328 dialog.run { |response|
3329 if response == Gtk::Dialog::RESPONSE_OK
3330 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3331 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3340 save_theme = theme_button.label
3341 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3342 save_opt432 = optimize432.active?
3343 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3344 save_nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3345 save_madewith = madewithentry.text.gsub('"', '"').gsub('\'', ''')
3346 save_indexlink = indexlinkentry.text.gsub('"', '"').gsub('\'', ''')
3349 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)
3350 mark_document_as_dirty
3352 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3353 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3354 (save_nperpage ? "--thumbnails-per-page #{save_nperpage} " : '') +
3355 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3356 utf8(_("Please wait while scanning source directory...")),
3358 { :closure_after => proc {
3359 open_file($filename)
3363 #- select_theme merges global variables, need to return to current choices
3364 select_current_theme
3371 sel = $albums_tv.selection.selected_rows
3373 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3374 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3375 utf8(_("Please wait while scanning source directory...")),
3377 { :closure_after => proc {
3378 open_file($filename)
3379 $albums_tv.selection.select_path(sel[0])
3387 sel = $albums_tv.selection.selected_rows
3389 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3390 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3391 utf8(_("Please wait while scanning source directory...")),
3393 { :closure_after => proc {
3394 open_file($filename)
3395 $albums_tv.selection.select_path(sel[0])
3403 theme = $xmldoc.root.attributes['theme']
3404 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3406 limit_sizes = "--sizes #{limit_sizes}"
3408 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3409 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3410 utf8(_("Please wait while scanning source directory...")),
3412 { :closure_after => proc {
3413 open_file($filename)
3419 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3421 Gtk::FileChooser::ACTION_SAVE,
3423 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3424 fc.transient_for = $main_window
3425 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3426 fc.set_current_folder(File.expand_path("~/.booh"))
3427 fc.filename = $orig_filename
3428 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3429 $orig_filename = fc.filename
3430 if ! save_current_file_user
3434 $config['last-opens'] ||= []
3435 $config['last-opens'] << $orig_filename
3441 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3443 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3444 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3445 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3447 dialog.vbox.add(notebook = Gtk::Notebook.new)
3448 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3449 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3450 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3451 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)),
3452 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3453 tooltips = Gtk::Tooltips.new
3454 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3455 for example: /usr/bin/mplayer %f")), nil)
3456 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3457 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3458 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3459 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3460 tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3461 for example: /usr/bin/gimp-remote %f")), nil)
3462 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3463 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3464 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3465 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3466 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3467 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3468 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3469 0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3470 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)),
3471 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3472 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)
3473 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3474 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3475 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)
3476 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3477 0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3478 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)
3480 smp_check.signal_connect('toggled') {
3481 if smp_check.active?
3482 smp_hbox.sensitive = true
3484 smp_hbox.sensitive = false
3488 smp_check.active = true
3489 smp_spin.value = $config['mproc'].to_i
3491 nogestures_check.active = $config['nogestures']
3492 deleteondisk_check.active = $config['deleteondisk']
3494 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3495 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3496 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3497 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3498 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)