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/gtkadds'
28 require 'booh/GtkAutoTable'
32 bindtextdomain("booh")
34 require 'rexml/document'
37 require 'booh/booh-lib'
39 require 'booh/UndoHandler'
44 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
46 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
49 #- default values for some globals
53 $ignore_videos = false
54 $button1_pressed_autotable = false
55 $generated_outofline = false
58 puts _("Usage: %s [OPTION]...") % File.basename($0)
60 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
65 parser = GetoptLong.new
66 parser.set_options(*$options.collect { |ary| ary[0..2] })
68 parser.each_option do |name, arg|
74 when '--verbose-level'
75 $verbose_level = arg.to_i
88 $config_file = File.expand_path('~/.booh-gui-rc')
89 if File.readable?($config_file)
90 $xmldoc = REXML::Document.new(File.new($config_file))
91 $xmldoc.root.elements.each { |element|
92 txt = element.get_text
94 if txt.value =~ /~~~/ || element.name == 'last-opens'
95 $config[element.name] = txt.value.split(/~~~/)
97 $config[element.name] = txt.value
99 elsif element.elements.size == 0
100 $config[element.name] = ''
102 $config[element.name] = {}
103 element.each { |chld|
105 $config[element.name][chld.name] = txt ? txt.value : nil
110 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
111 $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
112 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
113 $config['comments-format'] ||= '%t'
114 if !FileTest.directory?(File.expand_path('~/.booh'))
115 system("mkdir ~/.booh")
123 if !system("which convert >/dev/null 2>/dev/null")
124 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
125 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
128 if !system("which identify >/dev/null 2>/dev/null")
129 show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
130 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
132 missing = %w(transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
134 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
137 viewer_binary = $config['video-viewer'].split.first
138 if viewer_binary && !File.executable?(viewer_binary)
139 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
140 You should fix this in Edit/Preferences so that you can view videos.
142 Problem was: '%s' is not an executable file.
143 Hint: don't forget to specify the full path to the executable,
144 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
146 image_editor_binary = $config['image-editor'].split.first
147 if image_editor_binary && !File.executable?(image_editor_binary)
148 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
149 You should fix this in Edit/Preferences so that you can edit images externally.
151 Problem was: '%s' is not an executable file.
152 Hint: don't forget to specify the full path to the executable,
153 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
155 browser_binary = $config['browser'].split.first
156 if browser_binary && !File.executable?(browser_binary)
157 show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
158 You should fix this in Edit/Preferences so that you can open URLs.
160 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
165 if $config['last-opens'] && $config['last-opens'].size > 10
166 $config['last-opens'] = $config['last-opens'][-10, 10]
169 ios = File.open($config_file, "w")
170 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
171 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
172 $config.each_pair { |key, value|
173 elem = $xmldoc.root.add_element key
175 $config[key].each_pair { |subkey, subvalue|
176 subelem = elem.add_element subkey
177 subelem.add_text subvalue.to_s
179 elsif value.is_a? Array
180 elem.add_text value.join('~~~')
185 elem.add_text value.to_s
189 $xmldoc.write(ios, 0)
192 $tempfiles.each { |f|
197 def set_mousecursor(what, *widget)
198 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
199 if widget[0] && widget[0].window
200 widget[0].window.cursor = cursor
202 if $main_window && $main_window.window
203 $main_window.window.cursor = cursor
205 $current_cursor = what
207 def set_mousecursor_wait(*widget)
208 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
209 if Thread.current == Thread.main
210 Gtk.main_iteration while Gtk.events_pending?
213 def set_mousecursor_normal(*widget)
214 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
216 def push_mousecursor_wait(*widget)
217 if $current_cursor != Gdk::Cursor::WATCH
218 $save_cursor = $current_cursor
219 gtk_thread_protect { set_mousecursor_wait(*widget) }
222 def pop_mousecursor(*widget)
223 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
227 source = $xmldoc.root.attributes['source']
228 dest = $xmldoc.root.attributes['destination']
229 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
232 def full_src_dir_to_rel(path, source)
233 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
236 def build_full_dest_filename(filename)
237 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
240 def save_undo(name, closure, *params)
241 UndoHandler.save_undo(name, closure, [ *params ])
242 $undo_tb.sensitive = $undo_mb.sensitive = true
243 $redo_tb.sensitive = $redo_mb.sensitive = false
246 def view_element(filename, closures)
247 if entry2type(filename) == 'video'
248 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
254 w = Gtk::Window.new.set_title(filename)
256 msg 3, "filename: #{filename}"
257 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
258 #- typically this file won't exist in case of videos; try with the largest thumbnail around
259 if !File.exists?(dest_img)
260 if entry2type(filename) == 'video'
261 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
262 if not alternatives.empty?
263 dest_img = alternatives[-1]
266 push_mousecursor_wait
267 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
269 if !File.exists?(dest_img)
270 msg 2, _("Could not generate fullscreen thumbnail!")
275 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)))
276 evt.signal_connect('button-press-event') { |this, event|
277 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
278 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
280 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
282 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
283 delete_item.signal_connect('activate') {
285 closures[:delete].call
288 menu.popup(nil, nil, event.button, event.time)
291 evt.signal_connect('button-release-event') { |this, event|
293 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
294 msg 3, "gesture delete: click-drag right button to the bottom"
296 closures[:delete].call
297 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
301 tooltips = Gtk::Tooltips.new
302 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
304 w.signal_connect('key-press-event') { |w,event|
305 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
307 closures[:delete].call
311 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
312 b.signal_connect('clicked') { w.destroy }
315 vb.pack_start(evt, false, false)
316 vb.pack_end(bottom, false, false)
319 w.signal_connect('delete-event') { w.destroy }
320 w.window_position = Gtk::Window::POS_CENTER
324 def scroll_upper(scrolledwindow, ypos_top)
325 newval = scrolledwindow.vadjustment.value -
326 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
327 if newval < scrolledwindow.vadjustment.lower
328 newval = scrolledwindow.vadjustment.lower
330 scrolledwindow.vadjustment.value = newval
333 def scroll_lower(scrolledwindow, ypos_bottom)
334 newval = scrolledwindow.vadjustment.value +
335 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
336 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
337 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
339 scrolledwindow.vadjustment.value = newval
342 def autoscroll_if_needed(scrolledwindow, image, textview)
343 #- autoscroll if cursor or image is not visible, if possible
344 if image && image.window || textview.window
345 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
346 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
347 current_miny_visible = scrolledwindow.vadjustment.value
348 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
349 if ypos_top < current_miny_visible
350 scroll_upper(scrolledwindow, ypos_top)
351 elsif ypos_bottom > current_maxy_visible
352 scroll_lower(scrolledwindow, ypos_bottom)
357 def create_editzone(scrolledwindow, pagenum, image)
358 frame = Gtk::Frame.new
359 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
360 frame.set_shadow_type(Gtk::SHADOW_IN)
361 textview.signal_connect('key-press-event') { |w, event|
362 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
363 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
364 scrolledwindow.signal_emit('key-press-event', event)
366 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
367 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
368 if event.keyval == Gdk::Keyval::GDK_Up
369 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
370 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
372 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
375 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
376 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
378 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
385 candidate_undo_text = nil
386 textview.signal_connect('focus-in-event') { |w, event|
387 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
388 candidate_undo_text = textview.buffer.text
392 textview.signal_connect('key-release-event') { |w, event|
393 if candidate_undo_text && candidate_undo_text != textview.buffer.text
395 save_undo(_("text edit"),
397 save_text = textview.buffer.text
398 textview.buffer.text = text
400 $notebook.set_page(pagenum)
402 textview.buffer.text = save_text
404 $notebook.set_page(pagenum)
406 }, candidate_undo_text)
407 candidate_undo_text = nil
410 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)
411 autoscroll_if_needed(scrolledwindow, image, textview)
416 return [ frame, textview ]
419 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
421 if !$modified_pixbufs[thumbnail_img]
422 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
423 elsif !$modified_pixbufs[thumbnail_img][:orig]
424 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
427 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
430 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
431 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
432 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
433 if pixbuf.height > desired_y
434 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
435 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
436 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
441 if $modified_pixbufs[thumbnail_img][:whitebalance]
442 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
445 #- fix gamma correction
446 if $modified_pixbufs[thumbnail_img][:gammacorrect]
447 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
450 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
453 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
456 #- update rotate attribute
457 xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
459 $modified_pixbufs[thumbnail_img] ||= {}
460 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
461 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
463 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
466 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
469 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
471 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
473 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
474 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
476 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
477 $notebook.set_page(0)
478 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
483 def color_swap(xmldir, attributes_prefix)
485 if xmldir.attributes["#{attributes_prefix}color-swap"]
486 xmldir.delete_attribute("#{attributes_prefix}color-swap")
488 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
492 def enhance(xmldir, attributes_prefix)
494 if xmldir.attributes["#{attributes_prefix}enhance"]
495 xmldir.delete_attribute("#{attributes_prefix}enhance")
497 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
501 def change_frame_offset(xmldir, attributes_prefix, value)
503 xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
506 def ask_new_frame_offset(xmldir, attributes_prefix)
508 value = xmldir.attributes["#{attributes_prefix}frame-offset"]
513 dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
515 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
516 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
517 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
521 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
522 from. There are approximately 25 frames per second in a video.
525 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
526 entry.signal_connect('key-press-event') { |w, event|
527 if event.keyval == Gdk::Keyval::GDK_Return
528 dialog.response(Gtk::Dialog::RESPONSE_OK)
530 elsif event.keyval == Gdk::Keyval::GDK_Escape
531 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
534 false #- propagate if needed
538 dialog.window_position = Gtk::Window::POS_MOUSE
541 dialog.run { |response|
544 if response == Gtk::Dialog::RESPONSE_OK
546 msg 3, "changing frame offset to #{newval}"
547 return { :old => value, :new => newval }
554 def change_pano_amount(xmldir, attributes_prefix, value)
557 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
559 xmldir.add_attribute("#{attributes_prefix}pano-amount", value)
563 def ask_new_pano_amount(xmldir, attributes_prefix)
565 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
570 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
572 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
573 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
574 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
578 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
579 of this panorama image compared to other regular images. For example, if the panorama
580 was taken out of four photos on one row, counting the necessary overlap, the width of
581 this panorama image should probably be roughly three times the width of regular images.
583 With this information, booh will be able to generate panorama thumbnails looking
587 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)")))).
588 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
589 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
590 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
591 spin.signal_connect('value-changed') {
594 dialog.window_position = Gtk::Window::POS_MOUSE
597 spin.value = value.to_f
604 dialog.run { |response|
608 newval = spin.value.to_f
611 if response == Gtk::Dialog::RESPONSE_OK
613 msg 3, "changing panorama amount to #{newval}"
614 return { :old => value, :new => newval }
621 def change_whitebalance(xmlelem, attributes_prefix, value)
623 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
626 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
628 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
629 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
630 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
631 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
632 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
633 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
634 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
635 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
636 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
637 $modified_pixbufs[thumbnail_img] ||= {}
638 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
639 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
641 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
642 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
644 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
647 $modified_pixbufs[thumbnail_img] ||= {}
648 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
650 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
653 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
654 #- init $modified_pixbufs correctly
655 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
657 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
659 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
661 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
662 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
663 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
667 _("You can fix the <b>white balance</b> of the image, if your image is too blue
668 or too yellow because your camera didn't detect the light correctly. Drag the
669 slider below the image to the left for more blue, to the right for more yellow.
673 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
675 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
677 dialog.window_position = Gtk::Window::POS_MOUSE
681 timeout = Gtk.timeout_add(100) {
682 if hs.value != lastval
685 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
691 dialog.run { |response|
692 Gtk.timeout_remove(timeout)
693 if response == Gtk::Dialog::RESPONSE_OK
695 newval = hs.value.to_s
696 msg 3, "changing white balance to #{newval}"
698 return { :old => value, :new => newval }
701 $modified_pixbufs[thumbnail_img] ||= {}
702 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
703 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
711 def change_gammacorrect(xmlelem, attributes_prefix, value)
713 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
716 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
718 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
719 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
720 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
721 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
722 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
723 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
724 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
725 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
726 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
727 $modified_pixbufs[thumbnail_img] ||= {}
728 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
729 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
731 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
732 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
734 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
737 $modified_pixbufs[thumbnail_img] ||= {}
738 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
740 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
743 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
744 #- init $modified_pixbufs correctly
745 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
747 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
749 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
751 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
752 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
753 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
757 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
758 or too bright. Drag the slider below the image.
762 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
764 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
766 dialog.window_position = Gtk::Window::POS_MOUSE
770 timeout = Gtk.timeout_add(100) {
771 if hs.value != lastval
774 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
780 dialog.run { |response|
781 Gtk.timeout_remove(timeout)
782 if response == Gtk::Dialog::RESPONSE_OK
784 newval = hs.value.to_s
785 msg 3, "gamma correction to #{newval}"
787 return { :old => value, :new => newval }
790 $modified_pixbufs[thumbnail_img] ||= {}
791 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
792 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
800 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
801 system("rm -f '#{destfile}'")
802 #- type can be 'element' or 'subdir'
804 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
806 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
810 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
812 push_mousecursor_wait
813 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
816 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
822 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
823 distribute_multiple_call = Proc.new { |action, arg|
824 $selected_elements.each_key { |path|
825 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
827 if possible_actions[:can_multiple] && $selected_elements.length > 0
828 UndoHandler.begin_batch
829 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
830 UndoHandler.end_batch
832 closures[action].call(arg)
834 $selected_elements = {}
837 if optionals.include?('change_image')
838 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
839 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
840 changeimg.signal_connect('activate') { closures[:change].call }
841 menu.append(Gtk::SeparatorMenuItem.new)
843 if !possible_actions[:can_multiple] || $selected_elements.length == 0
846 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
847 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
848 view.signal_connect('activate') { closures[:view].call }
850 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
851 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
852 view.signal_connect('activate') { closures[:view].call }
853 menu.append(Gtk::SeparatorMenuItem.new)
856 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
857 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
858 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
859 exif.signal_connect('activate') { show_popup($main_window,
860 utf8(`identify -format "%[EXIF:*]" #{fullpath}`.sub(/MakerNote.*\n/, '')),
861 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
862 menu.append(Gtk::SeparatorMenuItem.new)
865 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
866 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
867 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
868 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
869 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
870 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
871 if !possible_actions[:can_multiple] || $selected_elements.length == 0
872 menu.append(Gtk::SeparatorMenuItem.new)
873 if !possible_actions[:forbid_left]
874 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
875 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
876 moveleft.signal_connect('activate') { closures[:move].call('left') }
877 if !possible_actions[:can_left]
878 moveleft.sensitive = false
881 if !possible_actions[:forbid_right]
882 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
883 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
884 moveright.signal_connect('activate') { closures[:move].call('right') }
885 if !possible_actions[:can_right]
886 moveright.sensitive = false
889 if optionals.include?('move_top')
890 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
891 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
892 movetop.signal_connect('activate') { closures[:move].call('top') }
893 if !possible_actions[:can_top]
894 movetop.sensitive = false
897 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
898 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
899 moveup.signal_connect('activate') { closures[:move].call('up') }
900 if !possible_actions[:can_up]
901 moveup.sensitive = false
903 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
904 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
905 movedown.signal_connect('activate') { closures[:move].call('down') }
906 if !possible_actions[:can_down]
907 movedown.sensitive = false
909 if optionals.include?('move_bottom')
910 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
911 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
912 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
913 if !possible_actions[:can_bottom]
914 movebottom.sensitive = false
919 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
920 menu.append(Gtk::SeparatorMenuItem.new)
921 menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
922 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
923 color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
924 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
925 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
926 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
927 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
928 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
929 frame_offset.signal_connect('activate') {
930 if possible_actions[:can_multiple] && $selected_elements.length > 0
931 if values = ask_new_frame_offset(nil, '')
932 distribute_multiple_call.call(:frame_offset, values)
935 closures[:frame_offset].call
940 menu.append( Gtk::SeparatorMenuItem.new)
941 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
942 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
943 gammacorrect.signal_connect('activate') {
944 if possible_actions[:can_multiple] && $selected_elements.length > 0
945 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
946 distribute_multiple_call.call(:gammacorrect, values)
949 closures[:gammacorrect].call
952 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
953 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
954 whitebalance.signal_connect('activate') {
955 if possible_actions[:can_multiple] && $selected_elements.length > 0
956 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
957 distribute_multiple_call.call(:whitebalance, values)
960 closures[:whitebalance].call
963 if !possible_actions[:can_multiple] || $selected_elements.length == 0
964 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
965 _("Enhance constrast"))))
967 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
969 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
970 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
971 if type == 'image' && possible_actions[:can_panorama]
972 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
973 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
974 panorama.signal_connect('activate') {
975 if possible_actions[:can_multiple] && $selected_elements.length > 0
976 if values = ask_new_pano_amount(nil, '')
977 distribute_multiple_call.call(:pano, values)
980 distribute_multiple_call.call(:pano)
984 menu.append( Gtk::SeparatorMenuItem.new)
985 if optionals.include?('delete')
986 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
987 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
988 if !possible_actions[:can_multiple] || $selected_elements.length == 0
989 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
990 paste_item.signal_connect('activate') { closures[:paste].call }
991 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
992 clear_item.signal_connect('activate') { $cuts = [] }
994 paste_item.sensitive = clear_item.sensitive = false
997 menu.append( Gtk::SeparatorMenuItem.new)
999 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1000 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1001 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1002 editexternally.signal_connect('activate') {
1003 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1008 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1009 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1010 if optionals.include?('delete')
1011 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1012 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1015 menu.popup(nil, nil, event.button, event.time)
1018 def delete_current_subalbum
1020 sel = $albums_tv.selection.selected_rows
1021 $xmldir.elements.each { |e|
1022 if e.name == 'image' || e.name == 'video'
1023 e.add_attribute('deleted', 'true')
1026 #- branch if we have a non deleted subalbum
1027 if $xmldir.child_byname_notattr('dir', 'deleted')
1028 $xmldir.delete_attribute('thumbnails-caption')
1029 $xmldir.delete_attribute('thumbnails-captionfile')
1031 $xmldir.add_attribute('deleted', 'true')
1033 while moveup.parent.name == 'dir'
1034 moveup = moveup.parent
1035 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1036 moveup.add_attribute('deleted', 'true')
1043 save_changes('forced')
1044 populate_subalbums_treeview(false)
1045 $albums_tv.selection.select_path(sel[0])
1051 $current_path = nil #- prevent save_changes from being rerun again
1052 sel = $albums_tv.selection.selected_rows
1053 restore_one = proc { |xmldir|
1054 xmldir.elements.each { |e|
1055 if e.name == 'dir' && e.attributes['deleted']
1058 e.delete_attribute('deleted')
1061 restore_one.call($xmldir)
1062 populate_subalbums_treeview(false)
1063 $albums_tv.selection.select_path(sel[0])
1066 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1069 frame1 = Gtk::Frame.new
1070 fullpath = from_utf8("#{$current_path}/#{filename}")
1072 my_gen_real_thumbnail = proc {
1073 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1077 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1078 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1079 pack_start(img = Gtk::Image.new).
1080 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1081 px, mask = pxb.render_pixmap_and_mask
1082 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1083 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1085 frame1.add(img = Gtk::Image.new)
1088 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1089 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1090 my_gen_real_thumbnail.call
1092 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1095 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1097 tooltips = Gtk::Tooltips.new
1098 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1099 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1101 frame2, textview = create_editzone($autotable_sw, 1, img)
1102 textview.buffer.text = caption
1103 textview.set_justification(Gtk::Justification::CENTER)
1105 vbox = Gtk::VBox.new(false, 5)
1106 vbox.pack_start(evtbox, false, false)
1107 vbox.pack_start(frame2, false, false)
1108 autotable.append(vbox, filename)
1110 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1111 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1113 #- to be able to find widgets by name
1114 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1116 cleanup_all_thumbnails = proc {
1117 #- remove out of sync images
1118 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1119 for sizeobj in $images_size
1120 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
1126 cleanup_all_thumbnails.call
1127 my_gen_real_thumbnail.call
1130 rotate_and_cleanup = proc { |angle|
1131 cleanup_all_thumbnails.call
1132 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1135 move = proc { |direction|
1136 do_method = "move_#{direction}"
1137 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1139 done = autotable.method(do_method).call(vbox)
1140 textview.grab_focus #- because if moving, focus is stolen
1144 save_undo(_("move %s") % direction,
1146 autotable.method(undo_method).call(vbox)
1147 textview.grab_focus #- because if moving, focus is stolen
1148 autoscroll_if_needed($autotable_sw, img, textview)
1149 $notebook.set_page(1)
1151 autotable.method(do_method).call(vbox)
1152 textview.grab_focus #- because if moving, focus is stolen
1153 autoscroll_if_needed($autotable_sw, img, textview)
1154 $notebook.set_page(1)
1160 color_swap_and_cleanup = proc {
1161 perform_color_swap_and_cleanup = proc {
1162 cleanup_all_thumbnails.call
1163 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1164 my_gen_real_thumbnail.call
1167 perform_color_swap_and_cleanup.call
1169 save_undo(_("color swap"),
1171 perform_color_swap_and_cleanup.call
1173 autoscroll_if_needed($autotable_sw, img, textview)
1174 $notebook.set_page(1)
1176 perform_color_swap_and_cleanup.call
1178 autoscroll_if_needed($autotable_sw, img, textview)
1179 $notebook.set_page(1)
1184 change_frame_offset_and_cleanup_real = proc { |values|
1185 perform_change_frame_offset_and_cleanup = proc { |val|
1186 cleanup_all_thumbnails.call
1187 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
1188 my_gen_real_thumbnail.call
1190 perform_change_frame_offset_and_cleanup.call(values[:new])
1192 save_undo(_("specify frame offset"),
1194 perform_change_frame_offset_and_cleanup.call(values[:old])
1196 autoscroll_if_needed($autotable_sw, img, textview)
1197 $notebook.set_page(1)
1199 perform_change_frame_offset_and_cleanup.call(values[:new])
1201 autoscroll_if_needed($autotable_sw, img, textview)
1202 $notebook.set_page(1)
1207 change_frame_offset_and_cleanup = proc {
1208 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
1209 change_frame_offset_and_cleanup_real.call(values)
1213 change_pano_amount_and_cleanup_real = proc { |values|
1214 perform_change_pano_amount_and_cleanup = proc { |val|
1215 cleanup_all_thumbnails.call
1216 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1218 perform_change_pano_amount_and_cleanup.call(values[:new])
1220 save_undo(_("change panorama amount"),
1222 perform_change_pano_amount_and_cleanup.call(values[:old])
1224 autoscroll_if_needed($autotable_sw, img, textview)
1225 $notebook.set_page(1)
1227 perform_change_pano_amount_and_cleanup.call(values[:new])
1229 autoscroll_if_needed($autotable_sw, img, textview)
1230 $notebook.set_page(1)
1235 change_pano_amount_and_cleanup = proc {
1236 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1237 change_pano_amount_and_cleanup_real.call(values)
1241 whitebalance_and_cleanup_real = proc { |values|
1242 perform_change_whitebalance_and_cleanup = proc { |val|
1243 cleanup_all_thumbnails.call
1244 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1245 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1246 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1248 perform_change_whitebalance_and_cleanup.call(values[:new])
1250 save_undo(_("fix white balance"),
1252 perform_change_whitebalance_and_cleanup.call(values[:old])
1254 autoscroll_if_needed($autotable_sw, img, textview)
1255 $notebook.set_page(1)
1257 perform_change_whitebalance_and_cleanup.call(values[:new])
1259 autoscroll_if_needed($autotable_sw, img, textview)
1260 $notebook.set_page(1)
1265 whitebalance_and_cleanup = proc {
1266 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1267 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1268 whitebalance_and_cleanup_real.call(values)
1272 gammacorrect_and_cleanup_real = proc { |values|
1273 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1274 cleanup_all_thumbnails.call
1275 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1276 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1277 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1279 perform_change_gammacorrect_and_cleanup.call(values[:new])
1281 save_undo(_("gamma correction"),
1283 perform_change_gammacorrect_and_cleanup.call(values[:old])
1285 autoscroll_if_needed($autotable_sw, img, textview)
1286 $notebook.set_page(1)
1288 perform_change_gammacorrect_and_cleanup.call(values[:new])
1290 autoscroll_if_needed($autotable_sw, img, textview)
1291 $notebook.set_page(1)
1296 gammacorrect_and_cleanup = Proc.new {
1297 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1298 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1299 gammacorrect_and_cleanup_real.call(values)
1303 enhance_and_cleanup = proc {
1304 perform_enhance_and_cleanup = proc {
1305 cleanup_all_thumbnails.call
1306 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1307 my_gen_real_thumbnail.call
1310 cleanup_all_thumbnails.call
1311 perform_enhance_and_cleanup.call
1313 save_undo(_("enhance"),
1315 perform_enhance_and_cleanup.call
1317 autoscroll_if_needed($autotable_sw, img, textview)
1318 $notebook.set_page(1)
1320 perform_enhance_and_cleanup.call
1322 autoscroll_if_needed($autotable_sw, img, textview)
1323 $notebook.set_page(1)
1328 delete = proc { |isacut|
1329 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 })
1332 perform_delete = proc {
1333 after = autotable.get_next_widget(vbox)
1335 after = autotable.get_previous_widget(vbox)
1337 if $config['deleteondisk'] && !isacut
1338 msg 3, "scheduling for delete: #{fullpath}"
1339 $todelete << fullpath
1341 autotable.remove(vbox)
1343 $vbox2widgets[after][:textview].grab_focus
1344 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1348 previous_pos = autotable.get_current_number(vbox)
1352 delete_current_subalbum
1354 save_undo(_("delete"),
1356 autotable.reinsert(pos, vbox, filename)
1357 $notebook.set_page(1)
1358 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1360 msg 3, "removing deletion schedule of: #{fullpath}"
1361 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1364 $notebook.set_page(1)
1373 $cuts << { :vbox => vbox, :filename => filename }
1374 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1379 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1382 autotable.queue_draws << proc {
1383 $vbox2widgets[last[:vbox]][:textview].grab_focus
1384 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1386 save_undo(_("paste"),
1388 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1389 $notebook.set_page(1)
1392 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1394 $notebook.set_page(1)
1397 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1402 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1403 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1404 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1405 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1407 textview.signal_connect('key-press-event') { |w, event|
1410 x, y = autotable.get_current_pos(vbox)
1411 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1412 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1413 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1414 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1416 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1417 $vbox2widgets[widget_up][:textview].grab_focus
1424 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1426 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1427 $vbox2widgets[widget_down][:textview].grab_focus
1434 if event.keyval == Gdk::Keyval::GDK_Left
1437 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1444 rotate_and_cleanup.call(-90)
1447 if event.keyval == Gdk::Keyval::GDK_Right
1448 next_ = autotable.get_next_widget(vbox)
1449 if next_ && autotable.get_current_pos(next_)[0] > x
1451 $vbox2widgets[next_][:textview].grab_focus
1458 rotate_and_cleanup.call(90)
1461 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1464 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1465 view_element(filename, { :delete => delete })
1468 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1471 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1475 !propagate #- propagate if needed
1478 $ignore_next_release = false
1479 evtbox.signal_connect('button-press-event') { |w, event|
1480 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1481 if event.state & Gdk::Window::BUTTON3_MASK != 0
1482 #- gesture redo: hold right mouse button then click left mouse button
1483 $config['nogestures'] or perform_redo
1484 $ignore_next_release = true
1486 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1488 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1490 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1491 elsif $enhance.active?
1492 enhance_and_cleanup.call
1493 elsif $delete.active?
1497 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1500 $button1_pressed_autotable = true
1501 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1502 if event.state & Gdk::Window::BUTTON1_MASK != 0
1503 #- gesture undo: hold left mouse button then click right mouse button
1504 $config['nogestures'] or perform_undo
1505 $ignore_next_release = true
1507 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1508 view_element(filename, { :delete => delete })
1513 evtbox.signal_connect('button-release-event') { |w, event|
1514 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1515 if !$ignore_next_release
1516 x, y = autotable.get_current_pos(vbox)
1517 next_ = autotable.get_next_widget(vbox)
1518 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1519 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1520 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1521 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1522 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1523 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1524 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1526 $ignore_next_release = false
1527 $gesture_press = nil
1532 #- handle reordering with drag and drop
1533 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1534 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1535 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1536 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1539 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1541 #- mouse gesture first (dnd disables button-release-event)
1542 if $gesture_press && $gesture_press[:filename] == filename
1543 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1544 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1545 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1546 rotate_and_cleanup.call(angle)
1547 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1549 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1550 msg 3, "gesture delete: click-drag right button to the bottom"
1552 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1557 ctxt.targets.each { |target|
1558 if target.name == 'reorder-elements'
1559 move_dnd = proc { |from,to|
1562 autotable.move(from, to)
1563 save_undo(_("reorder"),
1566 autotable.move(to - 1, from)
1568 autotable.move(to, from + 1)
1570 $notebook.set_page(1)
1572 autotable.move(from, to)
1573 $notebook.set_page(1)
1578 if $multiple_dnd.size == 0
1579 move_dnd.call(selection_data.data.to_i,
1580 autotable.get_current_number(vbox))
1582 UndoHandler.begin_batch
1583 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1585 #- need to update current position between each call
1586 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1587 autotable.get_current_number(vbox))
1589 UndoHandler.end_batch
1600 def create_auto_table
1602 $autotable = Gtk::AutoTable.new(5)
1604 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1605 thumbnails_vb = Gtk::VBox.new(false, 5)
1607 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1608 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1609 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1610 thumbnails_vb.add($autotable)
1612 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1613 $autotable_sw.add_with_viewport(thumbnails_vb)
1615 #- follows stuff for handling multiple elements selection
1616 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1618 update_selected = proc {
1619 $autotable.current_order.each { |path|
1620 w = $name2widgets[path][:evtbox].window
1621 xm = w.position[0] + w.size[0]/2
1622 ym = w.position[1] + w.size[1]/2
1623 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1624 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1625 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1626 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1629 if $selected_elements[path] && ! $selected_elements[path][:keep]
1630 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))
1631 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1632 $selected_elements.delete(path)
1637 $autotable.signal_connect('realize') { |w,e|
1638 gc = Gdk::GC.new($autotable.window)
1639 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1640 gc.function = Gdk::GC::INVERT
1641 #- autoscroll handling for DND and multiple selections
1642 Gtk.timeout_add(100) {
1643 if ! $autotable.window.nil?
1644 w, x, y, mask = $autotable.window.pointer
1645 if mask & Gdk::Window::BUTTON1_MASK != 0
1646 if y < $autotable_sw.vadjustment.value
1648 $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]])
1650 if $button1_pressed_autotable || press_x
1651 scroll_upper($autotable_sw, y)
1654 w, pos_x, pos_y = $autotable.window.pointer
1655 $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]])
1656 update_selected.call
1659 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1661 $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]])
1663 if $button1_pressed_autotable || press_x
1664 scroll_lower($autotable_sw, y)
1667 w, pos_x, pos_y = $autotable.window.pointer
1668 $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]])
1669 update_selected.call
1674 ! $autotable.window.nil?
1678 $autotable.signal_connect('button-press-event') { |w,e|
1680 if !$button1_pressed_autotable
1683 if e.state & Gdk::Window::SHIFT_MASK == 0
1684 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1685 $selected_elements = {}
1686 $statusbar.push(0, utf8(_("Nothing selected.")))
1688 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1690 set_mousecursor(Gdk::Cursor::TCROSS)
1694 $autotable.signal_connect('button-release-event') { |w,e|
1696 if $button1_pressed_autotable
1697 #- unselect all only now
1698 $multiple_dnd = $selected_elements.keys
1699 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1700 $selected_elements = {}
1701 $button1_pressed_autotable = false
1704 $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]])
1705 if $selected_elements.length > 0
1706 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1709 press_x = press_y = pos_x = pos_y = nil
1710 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1714 $autotable.signal_connect('motion-notify-event') { |w,e|
1717 $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]])
1721 $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]])
1722 update_selected.call
1728 def create_subalbums_page
1730 subalbums_hb = Gtk::HBox.new
1731 $subalbums_vb = Gtk::VBox.new(false, 5)
1732 subalbums_hb.pack_start($subalbums_vb, false, false)
1733 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1734 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1735 $subalbums_sw.add_with_viewport(subalbums_hb)
1738 def save_current_file
1744 ios = File.open($filename, "w")
1745 $xmldoc.write(ios, 0)
1747 rescue Iconv::IllegalSequence
1748 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1749 if ! ios.nil? && ! ios.closed?
1752 $xmldoc.xml_decl.encoding = 'UTF-8'
1753 ios = File.open($filename, "w")
1754 $xmldoc.write(ios, 0)
1765 def save_current_file_user
1766 save_tempfilename = $filename
1767 $filename = $orig_filename
1768 if ! save_current_file
1769 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1770 $filename = save_tempfilename
1774 $generated_outofline = false
1775 $filename = save_tempfilename
1777 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1778 $todelete.each { |f|
1779 system("rm -f #{f}")
1783 def mark_document_as_dirty
1784 $xmldoc.elements.each('//dir') { |elem|
1785 elem.delete_attribute('already-generated')
1789 #- ret: true => ok false => cancel
1790 def ask_save_modifications(msg1, msg2, *options)
1792 options = options.size > 0 ? options[0] : {}
1794 if options[:disallow_cancel]
1795 dialog = Gtk::Dialog.new(msg1,
1797 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1798 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1799 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1801 dialog = Gtk::Dialog.new(msg1,
1803 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1804 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1805 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1806 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1808 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1809 dialog.vbox.add(Gtk::Label.new(msg2))
1810 dialog.window_position = Gtk::Window::POS_CENTER
1813 dialog.run { |response|
1815 if response == Gtk::Dialog::RESPONSE_YES
1816 if ! save_current_file_user
1817 return ask_save_modifications(msg1, msg2, options)
1820 #- if we have generated an album but won't save modifications, we must remove
1821 #- already-generated markers in original file
1822 if $generated_outofline
1824 $xmldoc = REXML::Document.new File.new($orig_filename)
1825 mark_document_as_dirty
1826 ios = File.open($orig_filename, "w")
1827 $xmldoc.write(ios, 0)
1830 puts "exception: #{$!}"
1834 if response == Gtk::Dialog::RESPONSE_CANCEL
1837 $todelete = [] #- unconditionally clear the list of images/videos to delete
1843 def try_quit(*options)
1844 if ask_save_modifications(utf8(_("Save before quitting?")),
1845 utf8(_("Do you want to save your changes before quitting?")),
1851 def show_popup(parent, msg, *options)
1852 dialog = Gtk::Dialog.new
1853 if options[0] && options[0][:title]
1854 dialog.title = options[0][:title]
1856 dialog.title = utf8(_("Booh message"))
1858 lbl = Gtk::Label.new
1859 if options[0] && options[0][:nomarkup]
1864 if options[0] && options[0][:centered]
1865 lbl.set_justify(Gtk::Justification::CENTER)
1867 if options[0] && options[0][:selectable]
1868 lbl.selectable = true
1870 if options[0] && options[0][:topwidget]
1871 dialog.vbox.add(options[0][:topwidget])
1873 if options[0] && options[0][:scrolled]
1874 sw = Gtk::ScrolledWindow.new(nil, nil)
1875 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1876 sw.add_with_viewport(lbl)
1878 dialog.set_default_size(500, 600)
1880 dialog.vbox.add(lbl)
1881 dialog.set_default_size(200, 120)
1883 if options[0] && options[0][:okcancel]
1884 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1886 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1888 if options[0] && options[0][:pos_centered]
1889 dialog.window_position = Gtk::Window::POS_CENTER
1891 dialog.window_position = Gtk::Window::POS_MOUSE
1894 if options[0] && options[0][:linkurl]
1895 linkbut = Gtk::Button.new('')
1896 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1897 linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1898 linkbut.relief = Gtk::RELIEF_NONE
1899 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1900 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1901 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1906 if !options[0] || !options[0][:not_transient]
1907 dialog.transient_for = parent
1908 dialog.run { |response|
1910 if options[0] && options[0][:okcancel]
1911 return response == Gtk::Dialog::RESPONSE_OK
1915 dialog.signal_connect('response') { dialog.destroy }
1919 def backend_wait_message(parent, msg, infopipe_path, mode)
1921 w.set_transient_for(parent)
1924 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1925 vb.pack_start(Gtk::Label.new(msg), false, false)
1927 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1928 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1929 if mode != 'one dir scan'
1930 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1932 if mode == 'web-album'
1933 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1934 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1936 vb.pack_start(Gtk::HSeparator.new, false, false)
1938 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1939 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1940 vb.pack_end(bottom, false, false)
1942 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1943 refresh_thread = Thread.new {
1944 directories_counter = 0
1945 while line = infopipe.gets
1946 if line =~ /^directories: (\d+), sizes: (\d+)/
1947 directories = $1.to_f + 1
1949 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1950 elements = $3.to_f + 1
1951 if mode == 'web-album'
1955 gtk_thread_protect { pb1_1.fraction = 0 }
1956 if mode != 'one dir scan'
1957 newtext = utf8(full_src_dir_to_rel($1, $2))
1958 newtext = '/' if newtext == ''
1959 gtk_thread_protect { pb1_2.text = newtext }
1960 directories_counter += 1
1961 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1963 elsif line =~ /^processing element$/
1964 element_counter += 1
1965 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1966 elsif line =~ /^processing size$/
1967 element_counter += 1
1968 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1969 elsif line =~ /^finished processing sizes$/
1970 gtk_thread_protect { pb1_1.fraction = 1 }
1971 elsif line =~ /^creating index.html$/
1972 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1973 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1974 directories_counter = 0
1975 elsif line =~ /^index.html: (.+)\|(.+)/
1976 newtext = utf8(full_src_dir_to_rel($1, $2))
1977 newtext = '/' if newtext == ''
1978 gtk_thread_protect { pb2.text = newtext }
1979 directories_counter += 1
1980 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1981 elsif line =~ /^die: (.*)$/
1988 w.signal_connect('delete-event') { w.destroy }
1989 w.signal_connect('destroy') {
1990 Thread.kill(refresh_thread)
1991 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1994 system("rm -f #{infopipe_path}")
1997 w.window_position = Gtk::Window::POS_CENTER
2003 def call_backend(cmd, waitmsg, mode, params)
2004 pipe = Tempfile.new("boohpipe")
2006 system("mkfifo #{pipe.path}")
2007 cmd += " --info-pipe #{pipe.path}"
2008 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2013 id, exitstatus = Process.waitpid2(pid)
2014 gtk_thread_protect { w8.destroy }
2016 if params[:successmsg]
2017 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2019 if params[:closure_after]
2020 gtk_thread_protect(¶ms[:closure_after])
2022 elsif exitstatus == 15
2023 #- say nothing, user aborted
2025 gtk_thread_protect { show_popup($main_window,
2026 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2032 button.signal_connect('clicked') {
2033 Process.kill('SIGTERM', pid)
2037 def save_changes(*forced)
2038 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2042 $xmldir.delete_attribute('already-generated')
2044 propagate_children = proc { |xmldir|
2045 if xmldir.attributes['subdirs-caption']
2046 xmldir.delete_attribute('already-generated')
2048 xmldir.elements.each('dir') { |element|
2049 propagate_children.call(element)
2053 if $xmldir.child_byname_notattr('dir', 'deleted')
2054 new_title = $subalbums_title.buffer.text
2055 if new_title != $xmldir.attributes['subdirs-caption']
2056 parent = $xmldir.parent
2057 if parent.name == 'dir'
2058 parent.delete_attribute('already-generated')
2060 propagate_children.call($xmldir)
2062 $xmldir.add_attribute('subdirs-caption', new_title)
2063 $xmldir.elements.each('dir') { |element|
2064 if !element.attributes['deleted']
2065 path = element.attributes['path']
2066 newtext = $subalbums_edits[path][:editzone].buffer.text
2067 if element.attributes['subdirs-caption']
2068 if element.attributes['subdirs-caption'] != newtext
2069 propagate_children.call(element)
2071 element.add_attribute('subdirs-caption', newtext)
2072 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2074 if element.attributes['thumbnails-caption'] != newtext
2075 element.delete_attribute('already-generated')
2077 element.add_attribute('thumbnails-caption', newtext)
2078 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2084 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2085 if $xmldir.attributes['thumbnails-caption']
2086 path = $xmldir.attributes['path']
2087 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2089 elsif $xmldir.attributes['thumbnails-caption']
2090 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2093 if $xmldir.attributes['thumbnails-caption']
2094 if edit = $subalbums_edits[$xmldir.attributes['path']]
2095 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2099 #- remove and reinsert elements to reflect new ordering
2102 $xmldir.elements.each { |element|
2103 if element.name == 'image' || element.name == 'video'
2104 saves[element.attributes['filename']] = element.remove
2108 $autotable.current_order.each { |path|
2109 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2110 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2113 saves.each_key { |path|
2114 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2115 chld.add_attribute('deleted', 'true')
2119 def sort_by_exif_date
2123 $xmldir.elements.each { |element|
2124 if element.name == 'image' || element.name == 'video'
2125 current_order << element.attributes['filename']
2129 #- look for EXIF dates
2131 w.set_transient_for($main_window)
2133 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2134 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2135 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2136 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2137 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2138 vb.pack_end(bottom, false, false)
2140 w.signal_connect('delete-event') { w.destroy }
2141 w.window_position = Gtk::Window::POS_CENTER
2145 b.signal_connect('clicked') { aborted = true }
2148 current_order.each { |f|
2150 if entry2type(f) == 'image'
2152 pb.fraction = i.to_f / current_order.size
2153 Gtk.main_iteration while Gtk.events_pending?
2154 date_time = `identify -format "%[EXIF:DateTimeOriginal]" '#{from_utf8($current_path + "/" + f)}'`.chomp
2155 if $? == 0 && date_time != ''
2156 dates[f] = date_time
2169 $xmldir.elements.each { |element|
2170 if element.name == 'image' || element.name == 'video'
2171 saves[element.attributes['filename']] = element.remove
2175 #- find a good fallback for all entries without a date (still next to the item they were next to)
2176 neworder = dates.keys.sort { |a,b| dates[a] <=> dates[b] }
2177 for i in 0 .. current_order.size - 1
2178 if ! neworder.include?(current_order[i])
2180 while j > 0 && ! neworder.include?(current_order[j])
2183 neworder[(neworder.index(current_order[j]) || -1 ) + 1, 0] = current_order[i]
2187 $xmldir.add_element(saves[f].name, saves[f].attributes)
2190 #- let the auto-table reflect new ordering
2194 def remove_all_captions
2197 $autotable.current_order.each { |path|
2198 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2199 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2201 save_undo(_("remove all captions"),
2203 texts.each_key { |key|
2204 $name2widgets[key][:textview].buffer.text = texts[key]
2206 $notebook.set_page(1)
2208 texts.each_key { |key|
2209 $name2widgets[key][:textview].buffer.text = ''
2211 $notebook.set_page(1)
2217 $selected_elements.each_key { |path|
2218 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2224 $selected_elements = {}
2228 $undo_tb.sensitive = $undo_mb.sensitive = false
2229 $redo_tb.sensitive = $redo_mb.sensitive = false
2235 $subalbums_vb.children.each { |chld|
2236 $subalbums_vb.remove(chld)
2238 $subalbums = Gtk::Table.new(0, 0, true)
2239 current_y_sub_albums = 0
2241 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2242 $subalbums_edits = {}
2243 subalbums_counter = 0
2244 subalbums_edits_bypos = {}
2246 add_subalbum = proc { |xmldir, counter|
2247 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2248 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2249 if xmldir == $xmldir
2250 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2251 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2252 caption = xmldir.attributes['thumbnails-caption']
2253 infotype = 'thumbnails'
2255 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2256 captionfile, caption = find_subalbum_caption_info(xmldir)
2257 infotype = find_subalbum_info_type(xmldir)
2259 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2260 hbox = Gtk::HBox.new
2261 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2263 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2266 my_gen_real_thumbnail = proc {
2267 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2270 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2271 f.add(img = Gtk::Image.new)
2272 my_gen_real_thumbnail.call
2274 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2276 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2277 $subalbums.attach(hbox,
2278 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2280 frame, textview = create_editzone($subalbums_sw, 0, img)
2281 textview.buffer.text = caption
2282 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2283 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2285 change_image = proc {
2286 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2288 Gtk::FileChooser::ACTION_OPEN,
2290 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2291 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2292 fc.transient_for = $main_window
2293 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))
2294 f.add(preview_img = Gtk::Image.new)
2296 fc.signal_connect('update-preview') { |w|
2298 if fc.preview_filename
2299 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2300 fc.preview_widget_active = true
2302 rescue Gdk::PixbufError
2303 fc.preview_widget_active = false
2306 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2308 old_file = captionfile
2309 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2310 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2311 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2312 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2314 new_file = fc.filename
2315 msg 3, "new captionfile is: #{fc.filename}"
2316 perform_changefile = proc {
2317 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2318 $modified_pixbufs.delete(thumbnail_file)
2319 xmldir.delete_attribute("#{infotype}-rotate")
2320 xmldir.delete_attribute("#{infotype}-color-swap")
2321 xmldir.delete_attribute("#{infotype}-enhance")
2322 xmldir.delete_attribute("#{infotype}-frame-offset")
2323 my_gen_real_thumbnail.call
2325 perform_changefile.call
2327 save_undo(_("change caption file for sub-album"),
2329 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2330 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2331 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2332 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2333 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2334 my_gen_real_thumbnail.call
2335 $notebook.set_page(0)
2337 perform_changefile.call
2338 $notebook.set_page(0)
2346 system("rm -f '#{thumbnail_file}'")
2347 my_gen_real_thumbnail.call
2350 rotate_and_cleanup = proc { |angle|
2351 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2352 system("rm -f '#{thumbnail_file}'")
2355 move = proc { |direction|
2358 save_changes('forced')
2359 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2360 if direction == 'up'
2361 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2362 subalbums_edits_bypos[oldpos - 1][:position] += 1
2364 if direction == 'down'
2365 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2366 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2368 if direction == 'top'
2369 for i in 1 .. oldpos - 1
2370 subalbums_edits_bypos[i][:position] += 1
2372 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2374 if direction == 'bottom'
2375 for i in oldpos + 1 .. subalbums_counter
2376 subalbums_edits_bypos[i][:position] -= 1
2378 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2382 $xmldir.elements.each('dir') { |element|
2383 if (!element.attributes['deleted'])
2384 elems << [ element.attributes['path'], element.remove ]
2387 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2388 each { |e| $xmldir.add_element(e[1]) }
2389 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2390 $xmldir.elements.each('descendant::dir') { |elem|
2391 elem.delete_attribute('already-generated')
2394 sel = $albums_tv.selection.selected_rows
2396 populate_subalbums_treeview(false)
2397 $albums_tv.selection.select_path(sel[0])
2400 color_swap_and_cleanup = proc {
2401 perform_color_swap_and_cleanup = proc {
2402 color_swap(xmldir, "#{infotype}-")
2403 my_gen_real_thumbnail.call
2405 perform_color_swap_and_cleanup.call
2407 save_undo(_("color swap"),
2409 perform_color_swap_and_cleanup.call
2410 $notebook.set_page(0)
2412 perform_color_swap_and_cleanup.call
2413 $notebook.set_page(0)
2418 change_frame_offset_and_cleanup = proc {
2419 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2420 perform_change_frame_offset_and_cleanup = proc { |val|
2421 change_frame_offset(xmldir, "#{infotype}-", val)
2422 my_gen_real_thumbnail.call
2424 perform_change_frame_offset_and_cleanup.call(values[:new])
2426 save_undo(_("specify frame offset"),
2428 perform_change_frame_offset_and_cleanup.call(values[:old])
2429 $notebook.set_page(0)
2431 perform_change_frame_offset_and_cleanup.call(values[:new])
2432 $notebook.set_page(0)
2438 whitebalance_and_cleanup = proc {
2439 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2440 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2441 perform_change_whitebalance_and_cleanup = proc { |val|
2442 change_whitebalance(xmldir, "#{infotype}-", val)
2443 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2444 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2445 system("rm -f '#{thumbnail_file}'")
2447 perform_change_whitebalance_and_cleanup.call(values[:new])
2449 save_undo(_("fix white balance"),
2451 perform_change_whitebalance_and_cleanup.call(values[:old])
2452 $notebook.set_page(0)
2454 perform_change_whitebalance_and_cleanup.call(values[:new])
2455 $notebook.set_page(0)
2461 gammacorrect_and_cleanup = proc {
2462 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2463 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2464 perform_change_gammacorrect_and_cleanup = proc { |val|
2465 change_gammacorrect(xmldir, "#{infotype}-", val)
2466 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2467 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2468 system("rm -f '#{thumbnail_file}'")
2470 perform_change_gammacorrect_and_cleanup.call(values[:new])
2472 save_undo(_("gamma correction"),
2474 perform_change_gammacorrect_and_cleanup.call(values[:old])
2475 $notebook.set_page(0)
2477 perform_change_gammacorrect_and_cleanup.call(values[:new])
2478 $notebook.set_page(0)
2484 enhance_and_cleanup = proc {
2485 perform_enhance_and_cleanup = proc {
2486 enhance(xmldir, "#{infotype}-")
2487 my_gen_real_thumbnail.call
2490 perform_enhance_and_cleanup.call
2492 save_undo(_("enhance"),
2494 perform_enhance_and_cleanup.call
2495 $notebook.set_page(0)
2497 perform_enhance_and_cleanup.call
2498 $notebook.set_page(0)
2503 evtbox.signal_connect('button-press-event') { |w, event|
2504 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2506 rotate_and_cleanup.call(90)
2508 rotate_and_cleanup.call(-90)
2509 elsif $enhance.active?
2510 enhance_and_cleanup.call
2513 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2514 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2515 { :forbid_left => true, :forbid_right => true,
2516 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2517 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2518 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2519 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2520 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2522 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2527 evtbox.signal_connect('button-press-event') { |w, event|
2528 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2532 evtbox.signal_connect('button-release-event') { |w, event|
2533 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2534 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2535 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2536 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2537 msg 3, "gesture rotate: #{angle}"
2538 rotate_and_cleanup.call(angle)
2541 $gesture_press = nil
2544 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2545 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2546 current_y_sub_albums += 1
2549 if $xmldir.child_byname_notattr('dir', 'deleted')
2551 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2552 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2553 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2554 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2555 #- this album image/caption
2556 if $xmldir.attributes['thumbnails-caption']
2557 add_subalbum.call($xmldir, 0)
2560 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2561 $xmldir.elements.each { |element|
2562 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2563 #- element (image or video) of this album
2564 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2565 msg 3, "dest_img: #{dest_img}"
2566 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2567 total[element.name] += 1
2569 if element.name == 'dir' && !element.attributes['deleted']
2570 #- sub-album image/caption
2571 add_subalbum.call(element, subalbums_counter += 1)
2572 total[element.name] += 1
2575 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2576 total['image'], total['video'], total['dir'] ]))
2577 $subalbums_vb.add($subalbums)
2578 $subalbums_vb.show_all
2580 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2581 $notebook.get_tab_label($autotable_sw).sensitive = false
2582 $notebook.set_page(0)
2583 $thumbnails_title.buffer.text = ''
2585 $notebook.get_tab_label($autotable_sw).sensitive = true
2586 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2589 if !$xmldir.child_byname_notattr('dir', 'deleted')
2590 $notebook.get_tab_label($subalbums_sw).sensitive = false
2591 $notebook.set_page(1)
2593 $notebook.get_tab_label($subalbums_sw).sensitive = true
2597 def pixbuf_or_nil(filename)
2599 return Gdk::Pixbuf.new(filename)
2605 def theme_choose(current)
2606 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2608 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2609 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2610 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2612 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2613 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2614 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2615 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2616 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2617 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2618 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2619 treeview.signal_connect('button-press-event') { |w, event|
2620 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2621 dialog.response(Gtk::Dialog::RESPONSE_OK)
2625 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2627 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2630 iter[0] = File.basename(dir)
2631 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2632 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2633 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2634 if File.basename(dir) == current
2635 treeview.selection.select_iter(iter)
2639 dialog.set_default_size(700, 400)
2640 dialog.vbox.show_all
2641 dialog.run { |response|
2642 iter = treeview.selection.selected
2644 if response == Gtk::Dialog::RESPONSE_OK && iter
2645 return model.get_value(iter, 0)
2651 def show_password_protections
2652 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2653 child_iter = $albums_iters[xmldir.attributes['path']]
2654 if xmldir.attributes['password-protect']
2655 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2656 already_protected = true
2657 elsif already_protected
2658 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2660 pix = pix.saturate_and_pixelate(1, true)
2666 xmldir.elements.each('dir') { |elem|
2667 if !elem.attributes['deleted']
2668 examine_dir_elem.call(child_iter, elem, already_protected)
2672 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2675 def populate_subalbums_treeview(select_first)
2679 $subalbums_vb.children.each { |chld|
2680 $subalbums_vb.remove(chld)
2683 source = $xmldoc.root.attributes['source']
2684 msg 3, "source: #{source}"
2686 xmldir = $xmldoc.elements['//dir']
2687 if !xmldir || xmldir.attributes['path'] != source
2688 msg 1, _("Corrupted booh file...")
2692 append_dir_elem = proc { |parent_iter, xmldir|
2693 child_iter = $albums_ts.append(parent_iter)
2694 child_iter[0] = File.basename(xmldir.attributes['path'])
2695 child_iter[1] = xmldir.attributes['path']
2696 $albums_iters[xmldir.attributes['path']] = child_iter
2697 msg 3, "puttin location: #{xmldir.attributes['path']}"
2698 xmldir.elements.each('dir') { |elem|
2699 if !elem.attributes['deleted']
2700 append_dir_elem.call(child_iter, elem)
2704 append_dir_elem.call(nil, xmldir)
2705 show_password_protections
2707 $albums_tv.expand_all
2709 $albums_tv.selection.select_iter($albums_ts.iter_first)
2713 def select_current_theme
2714 select_theme($xmldoc.root.attributes['theme'],
2715 $xmldoc.root.attributes['limit-sizes'],
2716 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2717 $xmldoc.root.attributes['thumbnails-per-row'])
2720 def open_file(filename)
2724 $current_path = nil #- invalidate
2725 $modified_pixbufs = {}
2728 $subalbums_vb.children.each { |chld|
2729 $subalbums_vb.remove(chld)
2732 if !File.exists?(filename)
2733 return utf8(_("File not found."))
2737 $xmldoc = REXML::Document.new File.new(filename)
2742 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2743 if entry2type(filename).nil?
2744 return utf8(_("Not a booh file!"))
2746 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."))
2750 if !source = $xmldoc.root.attributes['source']
2751 return utf8(_("Corrupted booh file..."))
2754 if !dest = $xmldoc.root.attributes['destination']
2755 return utf8(_("Corrupted booh file..."))
2758 if !theme = $xmldoc.root.attributes['theme']
2759 return utf8(_("Corrupted booh file..."))
2762 if $xmldoc.root.attributes['version'] < '0.8.6'
2763 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2764 mark_document_as_dirty
2765 if $xmldoc.root.attributes['version'] < '0.8.4'
2766 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2767 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2768 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2769 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2770 if old_dest_dir != new_dest_dir
2771 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2773 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2774 xmldir.elements.each { |element|
2775 if %w(image video).include?(element.name) && !element.attributes['deleted']
2776 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2777 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2778 Dir[old_name + '*'].each { |file|
2779 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2780 file != new_file and sys("mv '#{file}' '#{new_file}'")
2783 if element.name == 'dir' && !element.attributes['deleted']
2784 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2785 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2786 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2790 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2794 $xmldoc.root.add_attribute('version', $VERSION)
2797 select_current_theme
2799 $filename = filename
2800 $default_size['thumbnails'] =~ /(.*)x(.*)/
2801 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2802 $albums_thumbnail_size =~ /(.*)x(.*)/
2803 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2805 populate_subalbums_treeview(true)
2807 $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
2811 def open_file_user(filename)
2812 result = open_file(filename)
2814 $config['last-opens'] ||= []
2815 if $config['last-opens'][-1] != utf8(filename)
2816 $config['last-opens'] << utf8(filename)
2818 $orig_filename = $filename
2819 tmp = Tempfile.new("boohtemp")
2822 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2824 $tempfiles << $filename << "#{$filename}.backup"
2826 $orig_filename = nil
2832 if !ask_save_modifications(utf8(_("Save this album?")),
2833 utf8(_("Do you want to save the changes to this album?")),
2834 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2837 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2839 Gtk::FileChooser::ACTION_OPEN,
2841 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2842 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2843 fc.set_current_folder(File.expand_path("~/.booh"))
2844 fc.transient_for = $main_window
2847 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2848 push_mousecursor_wait(fc)
2849 msg = open_file_user(fc.filename)
2865 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2870 def additional_booh_options
2873 options += "--mproc #{$config['mproc'].to_i} "
2875 options += "--comments-format '#{$config['comments-format']}'"
2880 if !ask_save_modifications(utf8(_("Save this album?")),
2881 utf8(_("Do you want to save the changes to this album?")),
2882 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2885 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2887 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2888 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2889 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2891 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2892 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2893 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2894 tbl.attach(src = Gtk::Entry.new,
2895 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2896 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2897 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2898 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2899 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2900 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2901 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2902 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2903 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2904 tbl.attach(dest = Gtk::Entry.new,
2905 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2906 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2907 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2908 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2909 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2910 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2911 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2912 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2913 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2915 tooltips = Gtk::Tooltips.new
2916 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2917 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2918 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2919 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2920 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2921 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2922 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)
2923 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2924 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2925 nperpage_model = Gtk::ListStore.new(String, String)
2926 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
2927 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
2928 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
2929 nperpagecombo.set_attributes(crt, { :markup => 0 })
2930 iter = nperpage_model.append
2931 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
2933 [ 12, 20, 30, 40, 50 ].each { |v|
2934 iter = nperpage_model.append
2935 iter[0] = iter[1] = v.to_s
2937 nperpagecombo.active = 0
2938 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
2939 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
2940 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)
2941 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2942 pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
2943 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)
2945 src_nb_calculated_for = ''
2947 process_src_nb = proc {
2948 if src.text != src_nb_calculated_for
2949 src_nb_calculated_for = src.text
2951 Thread.kill(src_nb_thread)
2954 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
2955 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
2957 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2958 if File.readable?(from_utf8_safe(src_nb_calculated_for))
2959 src_nb_thread = Thread.new {
2960 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2961 total = { 'image' => 0, 'video' => 0, nil => 0 }
2962 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2963 if File.basename(dir) =~ /^\./
2967 Dir.entries(dir.chomp).each { |file|
2968 total[entry2type(file)] += 1
2970 rescue Errno::EACCES, Errno::ENOENT
2974 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2978 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2981 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2987 timeout_src_nb = Gtk.timeout_add(100) {
2991 src_browse.signal_connect('clicked') {
2992 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2994 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2996 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2997 fc.transient_for = $main_window
2998 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2999 src.text = utf8(fc.filename)
3001 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3006 dest_browse.signal_connect('clicked') {
3007 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3009 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3011 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3012 fc.transient_for = $main_window
3013 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3014 dest.text = utf8(fc.filename)
3019 conf_browse.signal_connect('clicked') {
3020 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3022 Gtk::FileChooser::ACTION_SAVE,
3024 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3025 fc.transient_for = $main_window
3026 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3027 fc.set_current_folder(File.expand_path("~/.booh"))
3028 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3029 conf.text = utf8(fc.filename)
3036 recreate_theme_config = proc {
3037 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3039 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3040 $images_size.each { |s|
3041 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3045 tooltips.set_tip(cb, utf8(s['description']), nil)
3046 theme_sizes << { :widget => cb, :value => s['name'] }
3048 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3049 tooltips = Gtk::Tooltips.new
3050 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3051 theme_sizes << { :widget => cb, :value => 'original' }
3054 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3057 $allowed_N_values.each { |n|
3059 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3061 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3063 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3067 nperrows << { :widget => rb, :value => n }
3069 nperrowradios.show_all
3071 recreate_theme_config.call
3073 theme_button.signal_connect('clicked') {
3074 if newtheme = theme_choose(theme_button.label)
3075 theme_button.label = newtheme
3076 recreate_theme_config.call
3080 dialog.vbox.add(frame1)
3081 dialog.vbox.add(frame2)
3087 dialog.run { |response|
3088 if response == Gtk::Dialog::RESPONSE_OK
3089 srcdir = from_utf8_safe(src.text)
3090 destdir = from_utf8_safe(dest.text)
3091 confpath = from_utf8_safe(conf.text)
3092 if src.text != '' && srcdir == ''
3093 show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3095 elsif !File.directory?(srcdir)
3096 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3098 elsif dest.text != '' && destdir == ''
3099 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3101 elsif destdir != make_dest_filename(destdir)
3102 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3104 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3105 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
3107 elsif File.exists?(destdir) && !File.directory?(destdir)
3108 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3110 elsif conf.text == ''
3111 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3113 elsif conf.text != '' && confpath == ''
3114 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3116 elsif File.directory?(confpath)
3117 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3119 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3120 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3122 system("mkdir '#{destdir}'")
3123 if !File.directory?(destdir)
3124 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3136 srcdir = from_utf8(src.text)
3137 destdir = from_utf8(dest.text)
3138 configskel = File.expand_path(from_utf8(conf.text))
3139 theme = theme_button.label
3140 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3141 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3142 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3143 opt432 = optimize432.active?
3144 madewith = madewithentry.text
3145 indexlink = indexlinkentry.text
3148 Thread.kill(src_nb_thread)
3149 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3152 Gtk.timeout_remove(timeout_src_nb)
3155 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3156 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3157 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3158 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3159 utf8(_("Please wait while scanning source directory...")),
3161 { :closure_after => proc { open_file_user(configskel) } })
3166 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3168 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3169 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3170 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3172 source = $xmldoc.root.attributes['source']
3173 dest = $xmldoc.root.attributes['destination']
3174 theme = $xmldoc.root.attributes['theme']
3175 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3176 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3177 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3178 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3180 limit_sizes = limit_sizes.split(/,/)
3182 madewith = $xmldoc.root.attributes['made-with']
3183 indexlink = $xmldoc.root.attributes['index-link']
3185 tooltips = Gtk::Tooltips.new
3186 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3187 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3188 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3189 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3190 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3191 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3192 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3193 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3194 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3195 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3196 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3197 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3198 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3200 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3201 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3202 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3203 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3204 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3205 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3206 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)
3207 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3208 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3209 nperpage_model = Gtk::ListStore.new(String, String)
3210 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3211 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3212 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3213 nperpagecombo.set_attributes(crt, { :markup => 0 })
3214 iter = nperpage_model.append
3215 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3217 [ 12, 20, 30, 40, 50 ].each { |v|
3218 iter = nperpage_model.append
3219 iter[0] = iter[1] = v.to_s
3220 if nperpage && nperpage == v.to_s
3221 nperpagecombo.active_iter = iter
3224 if nperpagecombo.active_iter.nil?
3225 nperpagecombo.active = 0
3228 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3229 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3231 indexlinkentry.text = indexlink
3233 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)
3234 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3235 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3237 madewithentry.text = madewith
3239 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)
3243 recreate_theme_config = proc {
3244 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3246 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3248 $images_size.each { |s|
3249 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3251 if limit_sizes.include?(s['name'])
3259 tooltips.set_tip(cb, utf8(s['description']), nil)
3260 theme_sizes << { :widget => cb, :value => s['name'] }
3262 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3263 tooltips = Gtk::Tooltips.new
3264 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3265 if limit_sizes && limit_sizes.include?('original')
3268 theme_sizes << { :widget => cb, :value => 'original' }
3271 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3274 $allowed_N_values.each { |n|
3276 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3278 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3280 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3281 nperrowradios.add(Gtk::Label.new(' '))
3282 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3285 nperrows << { :widget => rb, :value => n.to_s }
3287 nperrowradios.show_all
3289 recreate_theme_config.call
3291 theme_button.signal_connect('clicked') {
3292 if newtheme = theme_choose(theme_button.label)
3295 theme_button.label = newtheme
3296 recreate_theme_config.call
3300 dialog.vbox.add(frame1)
3301 dialog.vbox.add(frame2)
3307 dialog.run { |response|
3308 if response == Gtk::Dialog::RESPONSE_OK
3309 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3310 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3319 save_theme = theme_button.label
3320 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3321 save_opt432 = optimize432.active?
3322 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3323 save_nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3324 save_madewith = madewithentry.text
3325 save_indexlink = indexlinkentry.text
3328 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)
3329 mark_document_as_dirty
3331 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3332 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3333 (save_nperpage ? "--thumbnails-per-page #{save_nperpage} " : '') +
3334 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3335 utf8(_("Please wait while scanning source directory...")),
3337 { :closure_after => proc {
3338 open_file($filename)
3342 #- select_theme merges global variables, need to return to current choices
3343 select_current_theme
3350 sel = $albums_tv.selection.selected_rows
3352 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3353 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3354 utf8(_("Please wait while scanning source directory...")),
3356 { :closure_after => proc {
3357 open_file($filename)
3358 $albums_tv.selection.select_path(sel[0])
3366 sel = $albums_tv.selection.selected_rows
3368 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3369 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3370 utf8(_("Please wait while scanning source directory...")),
3372 { :closure_after => proc {
3373 open_file($filename)
3374 $albums_tv.selection.select_path(sel[0])
3382 theme = $xmldoc.root.attributes['theme']
3383 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3385 limit_sizes = "--sizes #{limit_sizes}"
3387 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3388 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3389 utf8(_("Please wait while scanning source directory...")),
3391 { :closure_after => proc {
3392 open_file($filename)
3398 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3400 Gtk::FileChooser::ACTION_SAVE,
3402 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3403 fc.transient_for = $main_window
3404 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3405 fc.set_current_folder(File.expand_path("~/.booh"))
3406 fc.filename = $orig_filename
3407 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3408 $orig_filename = fc.filename
3409 if ! save_current_file_user
3413 $config['last-opens'] ||= []
3414 $config['last-opens'] << $orig_filename
3420 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3422 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3423 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3424 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3426 dialog.vbox.add(notebook = Gtk::Notebook.new)
3427 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3428 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3429 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3430 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)),
3431 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3432 tooltips = Gtk::Tooltips.new
3433 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3434 for example: /usr/bin/mplayer %f")), nil)
3435 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3436 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3437 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3438 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3439 tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3440 for example: /usr/bin/gimp-remote %f")), nil)
3441 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3442 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3443 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3444 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3445 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3446 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3447 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3448 0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3449 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)),
3450 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3451 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)
3452 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3453 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3454 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)
3455 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3456 0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3457 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)
3459 smp_check.signal_connect('toggled') {
3460 if smp_check.active?
3461 smp_hbox.sensitive = true
3463 smp_hbox.sensitive = false
3467 smp_check.active = true
3468 smp_spin.value = $config['mproc'].to_i
3470 nogestures_check.active = $config['nogestures']
3471 deleteondisk_check.active = $config['deleteondisk']
3473 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3474 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3475 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3476 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3477 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3478 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3479 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3480 tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3481 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3482 tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3483 2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3484 tooltips.set_tip(commentsformat_entry, utf8(_("Normally, filenames without extension are used as comments for images and videos in new albums. Use this entry to use something else for images.")), nil)
3485 commentsformat_help.signal_connect('clicked') {
3486 show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3487 hence you should look at ImageMagick/identify documentation for the most
3488 accurate and up-to-date documentation. Last time I checked, documentation
3491 Print information about the image in a format of your choosing. You can
3492 include the image filename, type, width, height, Exif data, or other image
3493 attributes by embedding special format characters:
3496 %P page width and height
3500 %e filename extension
3505 %k number of unique colors
3512 %r image class and colorspace
3515 %u unique temporary filename
3528 displays MIFF:bird.miff 512x480 for an image titled bird.miff and whose
3529 width is 512 and height is 480.
3531 If the first character of string is @, the format is read from a file titled
3532 by the remaining characters in the string.
3534 You can also use the following special formatting syntax to print Exif
3535 information contained in the file:
3539 Where tag can be one of the following:
3541 * (print all Exif tags, in keyword=data format)
3542 ! (print all Exif tags, in tag_number data format)
3543 #hhhh (print data for Exif tag #hhhh)
3548 PhotometricInterpretation
3568 PrimaryChromaticities
3571 JPEGInterchangeFormat
3572 JPEGInterchangeFormatLength
3594 ComponentsConfiguration
3595 CompressedBitsPerPixel
3615 InteroperabilityOffset
3617 SpatialFrequencyResponse
3618 FocalPlaneXResolution
3619 FocalPlaneYResolution
3620 FocalPlaneResolutionUnit
3625 SceneType")), { :scrolled => true })
3628 dialog.vbox.show_all
3629 dialog.run { |response|
3630 if response == Gtk::Dialog::RESPONSE_OK
3631 $config['video-viewer'] = from_utf8(video_viewer_entry.text)
3632 $config['image-editor'] = from_utf8(image_editor_entry.text)
3633 $config['browser'] = from_utf8(browser_entry.text)
3634 if smp_check.active?
3635 $config['mproc'] = smp_spin.value.to_i
3637 $config.delete('mproc')
3639 $config['nogestures'] = nogestures_check.active?
3640 $config['deleteondisk'] = deleteondisk_check.active?
3642 $config['convert-enhance'] = from_utf8(enhance_entry.text)
3643 $config['comments-format'] = from_utf8(commentsformat_entry.text.gsub(/'/, ''))
3650 if $undo_tb.sensitive?
3651 $redo_tb.sensitive = $redo_mb.sensitive = true
3652 if not more_undoes = UndoHandler.undo($statusbar)
3653 $undo_tb.sensitive = $undo_mb.sensitive = false
3659 if $redo_tb.sensitive?
3660 $undo_tb.sensitive = $undo_mb.sensitive = true
3661 if not more_redoes = UndoHandler.redo($statusbar)
3662 $redo_tb.sensitive = $redo_mb.sensitive = false
3667 def show_one_click_explanation(intro)
3668 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
3670 %s When such a tool is activated
3671 (<span foreground='darkblue'>Rotate clockwise</span>, <span foreground='darkblue'>Rotate counter-clockwise</span>, <span foreground='darkblue'>Enhance</span> or <span foreground='darkblue'>Delete</span>), clicking
3672 on a thumbnail will immediately apply the desired action.
3674 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
3675 ") % intro), { :pos_centered => true })
3680 GNU GENERAL PUBLIC LICENSE
3681 Version 2, June 1991
3683 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
3684 675 Mass Ave, Cambridge, MA 02139, USA
3685 Everyone is permitted to copy and distribute verbatim copies
3686 of this license document, but changing it is not allowed.
3690 The licenses for most software are designed to take away your
3691 freedom to share and change it. By contrast, the GNU General Public
3692 License is intended to guarantee your freedom to share and change free
3693 software--to make sure the software is free for all its users. This
3694 General Public License applies to most of the Free Software
3695 Foundation's software and to any other program whose authors commit to
3696 using it. (Some other Free Software Foundation software is covered by
3697 the GNU Library General Public License instead.) You can apply it to
3700 When we speak of free software, we are referring to freedom, not
3701 price. Our General Public Licenses are designed to make sure that you
3702 have the freedom to distribute copies of free software (and charge for
3703 this service if you wish), that you receive source code or can get it
3704 if you want it, that you can change the software or use pieces of it
3705 in new free programs; and that you know you can do these things.
3707 To protect your rights, we need to make restrictions that forbid
3708 anyone to deny you these rights or to ask you to surrender the rights.
3709 These restrictions translate to certain responsibilities for you if you
3710 distribute copies of the software, or if you modify it.
3712 For example, if you distribute copies of such a program, whether
3713 gratis or for a fee, you must give the recipients all the rights that
3714 you have. You must make sure that they, too, receive or can get the
3715 source code. And you must show them these terms so they know their
3718 We protect your rights with two steps: (1) copyright the software, and
3719 (2) offer you this license which gives you legal permission to copy,
3720 distribute and/or modify the software.
3722 Also, for each author's protection and ours, we want to make certain
3723 that everyone understands that there is no warranty for this free
3724 software. If the software is modified by someone else and passed on, we
3725 want its recipients to know that what they have is not the original, so
3726 that any problems introduced by others will not reflect on the original
3727 authors' reputations.
3729 Finally, any free program is threatened constantly by software
3730 patents. We wish to avoid the danger that redistributors of a free
3731 program will individually obtain patent licenses, in effect making the
3732 program proprietary. To prevent this, we have made it clear that any
3733 patent must be licensed for everyone's free use or not licensed at all.
3735 The precise terms and conditions for copying, distribution and
3736 modification follow.
3739 GNU GENERAL PUBLIC LICENSE
3740 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
3742 0. This License applies to any program or other work which contains
3743 a notice placed by the copyright holder saying it may be distributed
3744 under the terms of this General Public License. The "Program", below,
3745 refers to any such program or work, and a "work based on the Program"
3746 means either the Program or any derivative work under copyright law:
3747 that is to say, a work containing the Program or a portion of it,
3748 either verbatim or with modifications and/or translated into another
3749 language. (Hereinafter, translation is included without limitation in
3750 the term "modification".) Each licensee is addressed as "you".
3752 Activities other than copying, distribution and modification are not
3753 covered by this License; they are outside its scope. The act of
3754 running the Program is not restricted, and the output from the Program
3755 is covered only if its contents constitute a work based on the
3756 Program (independent of having been made by running the Program).
3757 Whether that is true depends on what the Program does.
3759 1. You may copy and distribute verbatim copies of the Program's
3760 source code as you receive it, in any medium, provided that you
3761 conspicuously and appropriately publish on each copy an appropriate
3762 copyright notice and disclaimer of warranty; keep intact all the
3763 notices that refer to this License and to the absence of any warranty;
3764 and give any other recipients of the Program a copy of this License
3765 along with the Program.
3767 You may charge a fee for the physical act of transferring a copy, and
3768 you may at your option offer warranty protection in exchange for a fee.
3770 2. You may modify your copy or copies of the Program or any portion
3771 of it, thus forming a work based on the Program, and copy and
3772 distribute such modifications or work under the terms of Section 1
3773 above, provided that you also meet all of these conditions:
3775 a) You must cause the modified files to carry prominent notices
3776 stating that you changed the files and the date of any change.
3778 b) You must cause any work that you distribute or publish, that in
3779 whole or in part contains or is derived from the Program or any
3780 part thereof, to be licensed as a whole at no charge to all third
3781 parties under the terms of this License.
3783 c) If the modified program normally reads commands interactively
3784 when run, you must cause it, when started running for such
3785 interactive use in the most ordinary way, to print or display an
3786 announcement including an appropriate copyright notice and a
3787 notice that there is no warranty (or else, saying that you provide
3788 a warranty) and that users may redistribute the program under
3789 these conditions, and telling the user how to view a copy of this
3790 License. (Exception: if the Program itself is interactive but
3791 does not normally print such an announcement, your work based on
3792 the Program is not required to print an announcement.)
3795 These requirements apply to the modified work as a whole. If
3796 identifiable sections of that work are not derived from the Program,
3797 and can be reasonably considered independent and separate works in
3798 themselves, then this License, and its terms, do not apply to those
3799 sections when you distribute them as separate works. But when you
3800 distribute the same sections as part of a whole which is a work based
3801 on the Program, the distribution of the whole must be on the terms of
3802 this License, whose permissions for other licensees extend to the
3803 entire whole, and thus to each and every part regardless of who wrote it.
3805 Thus, it is not the intent of this section to claim rights or contest
3806 your rights to work written entirely by you; rather, the intent is to
3807 exercise the right to control the distribution of derivative or
3808 collective works based on the Program.
3810 In addition, mere aggregation of another work not based on the Program
3811 with the Program (or with a work based on the Program) on a volume of
3812 a storage or distribution medium does not bring the other work under
3813 the scope of this License.
3815 3. You may copy and distribute the Program (or a work based on it,
3816 under Section 2) in object code or executable form under the terms of
3817 Sections 1 and 2 above provided that you also do one of the following:
3819 a) Accompany it with the complete corresponding machine-readable
3820 source code, which must be distributed under the terms of Sections
3821 1 and 2 above on a medium customarily used for software interchange; or,
3823 b) Accompany it with a written offer, valid for at least three
3824 years, to give any third party, for a charge no more than your
3825 cost of physically performing source distribution, a complete
3826 machine-readable copy of the corresponding source code, to be
3827 distributed under the terms of Sections 1 and 2 above on a medium
3828 customarily used for software interchange; or,
3830 c) Accompany it with the information you received as to the offer
3831 to distribute corresponding source code. (This alternative is
3832 allowed only for noncommercial distribution and only if you
3833 received the program in object code or executable form with such
3834 an offer, in accord with Subsection b above.)
3836 The source code for a work means the preferred form of the work for
3837 making modifications to it. For an executable work, complete source
3838 code means all the source code for all modules it contains, plus any
3839 associated interface definition files, plus the scripts used to
3840 control compilation and installation of the executable. However, as a
3841 special exception, the source code distributed need not include
3842 anything that is normally distributed (in either source or binary
3843 form) with the major components (compiler, kernel, and so on) of the
3844 operating system on which the executable runs, unless that component
3845 itself accompanies the executable.
3847 If distribution of executable or object code is made by offering
3848 access to copy from a designated place, then offering equivalent
3849 access to copy the source code from the same place counts as
3850 distribution of the source code, even though third parties are not
3851 compelled to copy the source along with the object code.
3854 4. You may not copy, modify, sublicense, or distribute the Program
3855 except as expressly provided under this License. Any attempt
3856 otherwise to copy, modify, sublicense or distribute the Program is
3857 void, and will automatically terminate your rights under this License.
3858 However, parties who have received copies, or rights, from you under
3859 this License will not have their licenses terminated so long as such
3860 parties remain in full compliance.
3862 5. You are not required to accept this License, since you have not
3863 signed it. However, nothing else grants you permission to modify or
3864 distribute the Program or its derivative works. These actions are
3865 prohibited by law if you do not accept this License. Therefore, by
3866 modifying or distributing the Program (or any work based on the
3867 Program), you indicate your acceptance of this License to do so, and
3868 all its terms and conditions for copying, distributing or modifying
3869 the Program or works based on it.
3871 6. Each time you redistribute the Program (or any work based on the
3872 Program), the recipient automatically receives a license from the
3873 original licensor to copy, distribute or modify the Program subject to
3874 these terms and conditions. You may not impose any further
3875 restrictions on the recipients' exercise of the rights granted herein.
3876 You are not responsible for enforcing compliance by third parties to
3879 7. If, as a consequence of a court judgment or allegation of patent
3880 infringement or for any other reason (not limited to patent issues),
3881 conditions are imposed on you (whether by court order, agreement or
3882 otherwise) that contradict the conditions of this License, they do not
3883 excuse you from the conditions of this License. If you cannot
3884 distribute so as to satisfy simultaneously your obligations under this
3885 License and any other pertinent obligations, then as a consequence you
3886 may not distribute the Program at all. For example, if a patent
3887 license would not permit royalty-free redistribution of the Program by
3888 all those who receive copies directly or indirectly through you, then
3889 the only way you could satisfy both it and this License would be to
3890 refrain entirely from distribution of the Program.
3892 If any portion of this section is held invalid or unenforceable under
3893 any particular circumstance, the balance of the section is intended to
3894 apply and the section as a whole is intended to apply in other
3897 It is not the purpose of this section to induce you to infringe any
3898 patents or other property right claims or to contest validity of any
3899 such claims; this section has the sole purpose of protecting the
3900 integrity of the free software distribution system, which is
3901 implemented by public license practices. Many people have made
3902 generous contributions to the wide range of software distributed
3903 through that system in reliance on consistent application of that
3904 system; it is up to the author/donor to decide if he or she is willing
3905 to distribute software through any other system and a licensee cannot
3908 This section is intended to make thoroughly clear what is believed to
3909 be a consequence of the rest of this License.
3912 8. If the distribution and/or use of the Program is restricted in
3913 certain countries either by patents or by copyrighted interfaces, the
3914 original copyright holder who places the Program under this License
3915 may add an explicit geographical distribution limitation excluding
3916 those countries, so that distribution is permitted only in or among
3917 countries not thus excluded. In such case, this License incorporates
3918 the limitation as if written in the body of this License.
3920 9. The Free Software Foundation may publish revised and/or new versions
3921 of the General Public License from time to time. Such new versions will
3922 be similar in spirit to the present version, but may differ in detail to
3923 address new problems or concerns.
3925 Each version is given a distinguishing version number. If the Program
3926 specifies a version number of this License which applies to it and "any
3927 later version", you have the option of following the terms and conditions
3928 either of that version or of any later version published by the Free
3929 Software Foundation. If the Program does not specify a version number of
3930 this License, you may choose any version ever published by the Free Software
3933 10. If you wish to incorporate parts of the Program into other free
3934 programs whose distribution conditions are different, write to the author
3935 to ask for permission. For software which is copyrighted by the Free
3936 Software Foundation, write to the Free Software Foundation; we sometimes
3937 make exceptions for this. Our decision will be guided by the two goals
3938 of preserving the free status of all derivatives of our free software and
3939 of promoting the sharing and reuse of software generally.
3943 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
3944 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
3945 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
3946 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
3947 OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
3948 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
3949 TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
3950 PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
3951 REPAIR OR CORRECTION.
3953 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
3954 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
3955 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
3956 INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
3957 OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
3958 TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
3959 YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
3960 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
3961 POSSIBILITY OF SUCH DAMAGES.
3965 def create_menu_and_toolbar
3968 mb = Gtk::MenuBar.new
3970 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
3971 filesubmenu = Gtk::Menu.new
3972 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
3973 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
3974 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3975 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
3976 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
3977 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3978 tooltips = Gtk::Tooltips.new
3979 filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
3980 $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3981 tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
3982 filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
3983 $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3984 tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
3985 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
3986 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3987 tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums (anywhere)")), nil)
3988 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3989 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
3990 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3991 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)