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 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., 675 Mass Ave, Cambridge, MA 02139, 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 dialog.window_position = Gtk::Window::POS_MOUSE
594 spin.value = value.to_f
601 dialog.run { |response|
605 newval = spin.value.to_f
608 if response == Gtk::Dialog::RESPONSE_OK
610 msg 3, "changing panorama amount to #{newval}"
611 return { :old => value, :new => newval }
618 def change_whitebalance(xmlelem, attributes_prefix, value)
620 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
623 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
625 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
626 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
627 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
628 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
629 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
630 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
631 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
632 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
633 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
634 $modified_pixbufs[thumbnail_img] ||= {}
635 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
636 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
638 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
639 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
641 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
644 $modified_pixbufs[thumbnail_img] ||= {}
645 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
647 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
650 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
651 #- init $modified_pixbufs correctly
652 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
654 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
656 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
658 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
659 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
660 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
664 _("You can fix the <b>white balance</b> of the image, if your image is too blue
665 or too yellow because your camera didn't detect the light correctly. Drag the
666 slider below the image to the left for more blue, to the right for more yellow.
670 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
672 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
674 dialog.window_position = Gtk::Window::POS_MOUSE
678 timeout = Gtk.timeout_add(100) {
679 if hs.value != lastval
682 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
688 dialog.run { |response|
689 Gtk.timeout_remove(timeout)
690 if response == Gtk::Dialog::RESPONSE_OK
692 newval = hs.value.to_s
693 msg 3, "changing white balance to #{newval}"
695 return { :old => value, :new => newval }
698 $modified_pixbufs[thumbnail_img] ||= {}
699 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
700 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
708 def change_gammacorrect(xmlelem, attributes_prefix, value)
710 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
713 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
715 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
716 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
717 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
718 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
719 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
720 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
721 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
722 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
723 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
724 $modified_pixbufs[thumbnail_img] ||= {}
725 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
726 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
728 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
729 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
731 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
734 $modified_pixbufs[thumbnail_img] ||= {}
735 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
737 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
740 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
741 #- init $modified_pixbufs correctly
742 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
744 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
746 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
748 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
749 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
750 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
754 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
755 or too bright. Drag the slider below the image.
759 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
761 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
763 dialog.window_position = Gtk::Window::POS_MOUSE
767 timeout = Gtk.timeout_add(100) {
768 if hs.value != lastval
771 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
777 dialog.run { |response|
778 Gtk.timeout_remove(timeout)
779 if response == Gtk::Dialog::RESPONSE_OK
781 newval = hs.value.to_s
782 msg 3, "gamma correction to #{newval}"
784 return { :old => value, :new => newval }
787 $modified_pixbufs[thumbnail_img] ||= {}
788 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
789 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
797 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
798 system("rm -f '#{destfile}'")
799 #- type can be 'element' or 'subdir'
801 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
803 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
807 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
809 push_mousecursor_wait
810 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
813 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
819 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
820 distribute_multiple_call = Proc.new { |action, arg|
821 $selected_elements.each_key { |path|
822 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
824 if possible_actions[:can_multiple] && $selected_elements.length > 0
825 UndoHandler.begin_batch
826 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
827 UndoHandler.end_batch
829 closures[action].call(arg)
831 $selected_elements = {}
834 if optionals.include?('change_image')
835 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
836 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
837 changeimg.signal_connect('activate') { closures[:change].call }
838 menu.append(Gtk::SeparatorMenuItem.new)
840 if !possible_actions[:can_multiple] || $selected_elements.length == 0
843 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
844 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
845 view.signal_connect('activate') { closures[:view].call }
847 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
848 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
849 view.signal_connect('activate') { closures[:view].call }
850 menu.append(Gtk::SeparatorMenuItem.new)
853 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
854 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
855 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
856 exif.signal_connect('activate') { show_popup($main_window,
857 utf8(`identify -format "%[EXIF:*]" #{fullpath}`.sub(/MakerNote.*\n/, '')),
858 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
859 menu.append(Gtk::SeparatorMenuItem.new)
862 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
863 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
864 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
865 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
866 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
867 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
868 if !possible_actions[:can_multiple] || $selected_elements.length == 0
869 menu.append(Gtk::SeparatorMenuItem.new)
870 if !possible_actions[:forbid_left]
871 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
872 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
873 moveleft.signal_connect('activate') { closures[:move].call('left') }
874 if !possible_actions[:can_left]
875 moveleft.sensitive = false
878 if !possible_actions[:forbid_right]
879 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
880 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
881 moveright.signal_connect('activate') { closures[:move].call('right') }
882 if !possible_actions[:can_right]
883 moveright.sensitive = false
886 if optionals.include?('move_top')
887 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
888 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
889 movetop.signal_connect('activate') { closures[:move].call('top') }
890 if !possible_actions[:can_top]
891 movetop.sensitive = false
894 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
895 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
896 moveup.signal_connect('activate') { closures[:move].call('up') }
897 if !possible_actions[:can_up]
898 moveup.sensitive = false
900 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
901 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
902 movedown.signal_connect('activate') { closures[:move].call('down') }
903 if !possible_actions[:can_down]
904 movedown.sensitive = false
906 if optionals.include?('move_bottom')
907 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
908 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
909 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
910 if !possible_actions[:can_bottom]
911 movebottom.sensitive = false
916 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
917 menu.append(Gtk::SeparatorMenuItem.new)
918 menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
919 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
920 color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
921 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
922 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
923 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
924 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
925 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
926 frame_offset.signal_connect('activate') {
927 if possible_actions[:can_multiple] && $selected_elements.length > 0
928 if values = ask_new_frame_offset(nil, '')
929 distribute_multiple_call.call(:frame_offset, values)
932 closures[:frame_offset].call
937 menu.append( Gtk::SeparatorMenuItem.new)
938 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
939 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
940 gammacorrect.signal_connect('activate') {
941 if possible_actions[:can_multiple] && $selected_elements.length > 0
942 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
943 distribute_multiple_call.call(:gammacorrect, values)
946 closures[:gammacorrect].call
949 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
950 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
951 whitebalance.signal_connect('activate') {
952 if possible_actions[:can_multiple] && $selected_elements.length > 0
953 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
954 distribute_multiple_call.call(:whitebalance, values)
957 closures[:whitebalance].call
960 if !possible_actions[:can_multiple] || $selected_elements.length == 0
961 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
962 _("Enhance constrast"))))
964 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
966 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
967 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
968 if type == 'image' && possible_actions[:can_panorama]
969 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
970 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
971 panorama.signal_connect('activate') {
972 if possible_actions[:can_multiple] && $selected_elements.length > 0
973 if values = ask_new_pano_amount(nil, '')
974 distribute_multiple_call.call(:pano, values)
977 distribute_multiple_call.call(:pano)
981 menu.append( Gtk::SeparatorMenuItem.new)
982 if optionals.include?('delete')
983 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
984 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
985 if !possible_actions[:can_multiple] || $selected_elements.length == 0
986 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
987 paste_item.signal_connect('activate') { closures[:paste].call }
988 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
989 clear_item.signal_connect('activate') { $cuts = [] }
991 paste_item.sensitive = clear_item.sensitive = false
994 menu.append( Gtk::SeparatorMenuItem.new)
996 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
997 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
998 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
999 editexternally.signal_connect('activate') {
1000 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1005 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1006 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1007 if optionals.include?('delete')
1008 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1009 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1012 menu.popup(nil, nil, event.button, event.time)
1015 def delete_current_subalbum
1017 sel = $albums_tv.selection.selected_rows
1018 $xmldir.elements.each { |e|
1019 if e.name == 'image' || e.name == 'video'
1020 e.add_attribute('deleted', 'true')
1023 #- branch if we have a non deleted subalbum
1024 if $xmldir.child_byname_notattr('dir', 'deleted')
1025 $xmldir.delete_attribute('thumbnails-caption')
1026 $xmldir.delete_attribute('thumbnails-captionfile')
1028 $xmldir.add_attribute('deleted', 'true')
1030 while moveup.parent.name == 'dir'
1031 moveup = moveup.parent
1032 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1033 moveup.add_attribute('deleted', 'true')
1040 save_changes('forced')
1041 populate_subalbums_treeview(false)
1042 $albums_tv.selection.select_path(sel[0])
1048 $current_path = nil #- prevent save_changes from being rerun again
1049 sel = $albums_tv.selection.selected_rows
1050 restore_one = proc { |xmldir|
1051 xmldir.elements.each { |e|
1052 if e.name == 'dir' && e.attributes['deleted']
1055 e.delete_attribute('deleted')
1058 restore_one.call($xmldir)
1059 populate_subalbums_treeview(false)
1060 $albums_tv.selection.select_path(sel[0])
1063 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1066 frame1 = Gtk::Frame.new
1067 fullpath = from_utf8("#{$current_path}/#{filename}")
1069 my_gen_real_thumbnail = proc {
1070 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1074 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1075 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1076 pack_start(img = Gtk::Image.new).
1077 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1078 px, mask = pxb.render_pixmap_and_mask
1079 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1080 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1082 frame1.add(img = Gtk::Image.new)
1085 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1086 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1087 my_gen_real_thumbnail.call
1089 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1092 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1094 tooltips = Gtk::Tooltips.new
1095 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1096 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1098 frame2, textview = create_editzone($autotable_sw, 1, img)
1099 textview.buffer.text = caption
1100 textview.set_justification(Gtk::Justification::CENTER)
1102 vbox = Gtk::VBox.new(false, 5)
1103 vbox.pack_start(evtbox, false, false)
1104 vbox.pack_start(frame2, false, false)
1105 autotable.append(vbox, filename)
1107 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1108 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1110 #- to be able to find widgets by name
1111 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1113 cleanup_all_thumbnails = proc {
1114 #- remove out of sync images
1115 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1116 for sizeobj in $images_size
1117 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
1123 cleanup_all_thumbnails.call
1124 my_gen_real_thumbnail.call
1127 rotate_and_cleanup = proc { |angle|
1128 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1129 cleanup_all_thumbnails.call
1132 move = proc { |direction|
1133 do_method = "move_#{direction}"
1134 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1136 done = autotable.method(do_method).call(vbox)
1137 textview.grab_focus #- because if moving, focus is stolen
1141 save_undo(_("move %s") % direction,
1143 autotable.method(undo_method).call(vbox)
1144 textview.grab_focus #- because if moving, focus is stolen
1145 autoscroll_if_needed($autotable_sw, img, textview)
1146 $notebook.set_page(1)
1148 autotable.method(do_method).call(vbox)
1149 textview.grab_focus #- because if moving, focus is stolen
1150 autoscroll_if_needed($autotable_sw, img, textview)
1151 $notebook.set_page(1)
1157 color_swap_and_cleanup = proc {
1158 perform_color_swap_and_cleanup = proc {
1159 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1160 my_gen_real_thumbnail.call
1163 cleanup_all_thumbnails.call
1164 perform_color_swap_and_cleanup.call
1166 save_undo(_("color swap"),
1168 perform_color_swap_and_cleanup.call
1170 autoscroll_if_needed($autotable_sw, img, textview)
1171 $notebook.set_page(1)
1173 perform_color_swap_and_cleanup.call
1175 autoscroll_if_needed($autotable_sw, img, textview)
1176 $notebook.set_page(1)
1181 change_frame_offset_and_cleanup_real = proc { |values|
1182 perform_change_frame_offset_and_cleanup = proc { |val|
1183 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
1184 my_gen_real_thumbnail.call
1186 perform_change_frame_offset_and_cleanup.call(values[:new])
1188 save_undo(_("specify frame offset"),
1190 perform_change_frame_offset_and_cleanup.call(values[:old])
1192 autoscroll_if_needed($autotable_sw, img, textview)
1193 $notebook.set_page(1)
1195 perform_change_frame_offset_and_cleanup.call(values[:new])
1197 autoscroll_if_needed($autotable_sw, img, textview)
1198 $notebook.set_page(1)
1203 change_frame_offset_and_cleanup = proc {
1204 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
1205 change_frame_offset_and_cleanup_real.call(values)
1209 change_pano_amount_and_cleanup_real = proc { |values|
1210 perform_change_pano_amount_and_cleanup = proc { |val|
1211 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1213 perform_change_pano_amount_and_cleanup.call(values[:new])
1215 save_undo(_("change panorama amount"),
1217 perform_change_pano_amount_and_cleanup.call(values[:old])
1219 autoscroll_if_needed($autotable_sw, img, textview)
1220 $notebook.set_page(1)
1222 perform_change_pano_amount_and_cleanup.call(values[:new])
1224 autoscroll_if_needed($autotable_sw, img, textview)
1225 $notebook.set_page(1)
1230 change_pano_amount_and_cleanup = proc {
1231 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1232 change_pano_amount_and_cleanup_real.call(values)
1236 whitebalance_and_cleanup_real = proc { |values|
1237 perform_change_whitebalance_and_cleanup = proc { |val|
1238 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1239 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1240 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1241 cleanup_all_thumbnails.call
1243 perform_change_whitebalance_and_cleanup.call(values[:new])
1245 save_undo(_("fix white balance"),
1247 perform_change_whitebalance_and_cleanup.call(values[:old])
1249 autoscroll_if_needed($autotable_sw, img, textview)
1250 $notebook.set_page(1)
1252 perform_change_whitebalance_and_cleanup.call(values[:new])
1254 autoscroll_if_needed($autotable_sw, img, textview)
1255 $notebook.set_page(1)
1260 whitebalance_and_cleanup = proc {
1261 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1262 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1263 whitebalance_and_cleanup_real.call(values)
1267 gammacorrect_and_cleanup_real = proc { |values|
1268 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1269 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1270 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1271 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1272 cleanup_all_thumbnails.call
1274 perform_change_gammacorrect_and_cleanup.call(values[:new])
1276 save_undo(_("gamma correction"),
1278 perform_change_gammacorrect_and_cleanup.call(values[:old])
1280 autoscroll_if_needed($autotable_sw, img, textview)
1281 $notebook.set_page(1)
1283 perform_change_gammacorrect_and_cleanup.call(values[:new])
1285 autoscroll_if_needed($autotable_sw, img, textview)
1286 $notebook.set_page(1)
1291 gammacorrect_and_cleanup = Proc.new {
1292 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1293 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1294 gammacorrect_and_cleanup_real.call(values)
1298 enhance_and_cleanup = proc {
1299 perform_enhance_and_cleanup = proc {
1300 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1301 my_gen_real_thumbnail.call
1304 cleanup_all_thumbnails.call
1305 perform_enhance_and_cleanup.call
1307 save_undo(_("enhance"),
1309 perform_enhance_and_cleanup.call
1311 autoscroll_if_needed($autotable_sw, img, textview)
1312 $notebook.set_page(1)
1314 perform_enhance_and_cleanup.call
1316 autoscroll_if_needed($autotable_sw, img, textview)
1317 $notebook.set_page(1)
1322 delete = proc { |isacut|
1323 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 })
1326 perform_delete = proc {
1327 after = autotable.get_next_widget(vbox)
1329 after = autotable.get_previous_widget(vbox)
1331 if $config['deleteondisk'] && !isacut
1332 msg 3, "scheduling for delete: #{fullpath}"
1333 $todelete << fullpath
1335 autotable.remove(vbox)
1337 $vbox2widgets[after][:textview].grab_focus
1338 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1342 previous_pos = autotable.get_current_number(vbox)
1346 delete_current_subalbum
1348 save_undo(_("delete"),
1350 autotable.reinsert(pos, vbox, filename)
1351 $notebook.set_page(1)
1352 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1354 msg 3, "removing deletion schedule of: #{fullpath}"
1355 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1358 $notebook.set_page(1)
1367 $cuts << { :vbox => vbox, :filename => filename }
1368 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1373 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1376 autotable.queue_draws << proc {
1377 $vbox2widgets[last[:vbox]][:textview].grab_focus
1378 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1380 save_undo(_("paste"),
1382 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1383 $notebook.set_page(1)
1386 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1388 $notebook.set_page(1)
1391 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1396 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1397 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1398 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1399 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1401 textview.signal_connect('key-press-event') { |w, event|
1404 x, y = autotable.get_current_pos(vbox)
1405 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1406 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1407 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1408 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1410 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1411 $vbox2widgets[widget_up][:textview].grab_focus
1418 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1420 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1421 $vbox2widgets[widget_down][:textview].grab_focus
1428 if event.keyval == Gdk::Keyval::GDK_Left
1431 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1438 rotate_and_cleanup.call(-90)
1441 if event.keyval == Gdk::Keyval::GDK_Right
1442 next_ = autotable.get_next_widget(vbox)
1443 if next_ && autotable.get_current_pos(next_)[0] > x
1445 $vbox2widgets[next_][:textview].grab_focus
1452 rotate_and_cleanup.call(90)
1455 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1458 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1459 view_element(filename, { :delete => delete })
1462 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1465 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1469 !propagate #- propagate if needed
1472 $ignore_next_release = false
1473 evtbox.signal_connect('button-press-event') { |w, event|
1474 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1475 if event.state & Gdk::Window::BUTTON3_MASK != 0
1476 #- gesture redo: hold right mouse button then click left mouse button
1477 $config['nogestures'] or perform_redo
1478 $ignore_next_release = true
1480 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1482 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1484 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1485 elsif $enhance.active?
1486 enhance_and_cleanup.call
1487 elsif $delete.active?
1491 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1494 $button1_pressed_autotable = true
1495 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1496 if event.state & Gdk::Window::BUTTON1_MASK != 0
1497 #- gesture undo: hold left mouse button then click right mouse button
1498 $config['nogestures'] or perform_undo
1499 $ignore_next_release = true
1501 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1502 view_element(filename, { :delete => delete })
1507 evtbox.signal_connect('button-release-event') { |w, event|
1508 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1509 if !$ignore_next_release
1510 x, y = autotable.get_current_pos(vbox)
1511 next_ = autotable.get_next_widget(vbox)
1512 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1513 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1514 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1515 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1516 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1517 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1518 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1520 $ignore_next_release = false
1521 $gesture_press = nil
1526 #- handle reordering with drag and drop
1527 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1528 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1529 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1530 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1533 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1535 #- mouse gesture first (dnd disables button-release-event)
1536 if $gesture_press && $gesture_press[:filename] == filename
1537 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1538 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1539 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1540 rotate_and_cleanup.call(angle)
1541 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1543 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1544 msg 3, "gesture delete: click-drag right button to the bottom"
1546 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1551 ctxt.targets.each { |target|
1552 if target.name == 'reorder-elements'
1553 move_dnd = proc { |from,to|
1556 autotable.move(from, to)
1557 save_undo(_("reorder"),
1560 autotable.move(to - 1, from)
1562 autotable.move(to, from + 1)
1564 $notebook.set_page(1)
1566 autotable.move(from, to)
1567 $notebook.set_page(1)
1572 if $multiple_dnd.size == 0
1573 move_dnd.call(selection_data.data.to_i,
1574 autotable.get_current_number(vbox))
1576 UndoHandler.begin_batch
1577 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1579 #- need to update current position between each call
1580 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1581 autotable.get_current_number(vbox))
1583 UndoHandler.end_batch
1594 def create_auto_table
1596 $autotable = Gtk::AutoTable.new(5)
1598 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1599 thumbnails_vb = Gtk::VBox.new(false, 5)
1601 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1602 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1603 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1604 thumbnails_vb.add($autotable)
1606 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1607 $autotable_sw.add_with_viewport(thumbnails_vb)
1609 #- follows stuff for handling multiple elements selection
1610 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1612 update_selected = proc {
1613 $autotable.current_order.each { |path|
1614 w = $name2widgets[path][:evtbox].window
1615 xm = w.position[0] + w.size[0]/2
1616 ym = w.position[1] + w.size[1]/2
1617 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1618 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1619 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1620 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1623 if $selected_elements[path] && ! $selected_elements[path][:keep]
1624 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))
1625 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1626 $selected_elements.delete(path)
1631 $autotable.signal_connect('realize') { |w,e|
1632 gc = Gdk::GC.new($autotable.window)
1633 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1634 gc.function = Gdk::GC::INVERT
1635 #- autoscroll handling for DND and multiple selections
1636 Gtk.timeout_add(100) {
1637 if ! $autotable.window.nil?
1638 w, x, y, mask = $autotable.window.pointer
1639 if mask & Gdk::Window::BUTTON1_MASK != 0
1640 if y < $autotable_sw.vadjustment.value
1642 $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]])
1644 if $button1_pressed_autotable || press_x
1645 scroll_upper($autotable_sw, y)
1648 w, pos_x, pos_y = $autotable.window.pointer
1649 $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 update_selected.call
1653 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
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]])
1657 if $button1_pressed_autotable || press_x
1658 scroll_lower($autotable_sw, y)
1661 w, pos_x, pos_y = $autotable.window.pointer
1662 $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 update_selected.call
1668 ! $autotable.window.nil?
1672 $autotable.signal_connect('button-press-event') { |w,e|
1674 if !$button1_pressed_autotable
1677 if e.state & Gdk::Window::SHIFT_MASK == 0
1678 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1679 $selected_elements = {}
1680 $statusbar.push(0, utf8(_("Nothing selected.")))
1682 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1684 set_mousecursor(Gdk::Cursor::TCROSS)
1688 $autotable.signal_connect('button-release-event') { |w,e|
1690 if $button1_pressed_autotable
1691 #- unselect all only now
1692 $multiple_dnd = $selected_elements.keys
1693 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1694 $selected_elements = {}
1695 $button1_pressed_autotable = false
1698 $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]])
1699 if $selected_elements.length > 0
1700 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1703 press_x = press_y = pos_x = pos_y = nil
1704 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1708 $autotable.signal_connect('motion-notify-event') { |w,e|
1711 $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]])
1715 $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]])
1716 update_selected.call
1722 def create_subalbums_page
1724 subalbums_hb = Gtk::HBox.new
1725 $subalbums_vb = Gtk::VBox.new(false, 5)
1726 subalbums_hb.pack_start($subalbums_vb, false, false)
1727 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1728 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1729 $subalbums_sw.add_with_viewport(subalbums_hb)
1732 def save_current_file
1738 ios = File.open($filename, "w")
1739 $xmldoc.write(ios, 0)
1741 rescue Iconv::IllegalSequence
1742 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1743 if ! ios.nil? && ! ios.closed?
1746 $xmldoc.xml_decl.encoding = 'UTF-8'
1747 ios = File.open($filename, "w")
1748 $xmldoc.write(ios, 0)
1758 def save_current_file_user
1759 save_tempfilename = $filename
1760 $filename = $orig_filename
1761 if ! save_current_file
1762 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1763 $filename = save_tempfilename
1767 $generated_outofline = false
1768 $filename = save_tempfilename
1770 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1771 $todelete.each { |f|
1772 system("rm -f #{f}")
1776 def mark_document_as_dirty
1777 $xmldoc.elements.each('//dir') { |elem|
1778 elem.delete_attribute('already-generated')
1782 #- ret: true => ok false => cancel
1783 def ask_save_modifications(msg1, msg2, *options)
1785 options = options.size > 0 ? options[0] : {}
1787 if options[:disallow_cancel]
1788 dialog = Gtk::Dialog.new(msg1,
1790 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1791 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1792 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1794 dialog = Gtk::Dialog.new(msg1,
1796 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1797 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1798 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1799 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1801 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1802 dialog.vbox.add(Gtk::Label.new(msg2))
1803 dialog.window_position = Gtk::Window::POS_CENTER
1806 dialog.run { |response|
1808 if response == Gtk::Dialog::RESPONSE_YES
1809 if ! save_current_file_user
1810 return ask_save_modifications(msg1, msg2, options)
1813 #- if we have generated an album but won't save modifications, we must remove
1814 #- already-generated markers in original file
1815 if $generated_outofline
1817 $xmldoc = REXML::Document.new File.new($orig_filename)
1818 mark_document_as_dirty
1819 ios = File.open($orig_filename, "w")
1820 $xmldoc.write(ios, 0)
1823 puts "exception: #{$!}"
1827 if response == Gtk::Dialog::RESPONSE_CANCEL
1830 $todelete = [] #- unconditionally clear the list of images/videos to delete
1836 def try_quit(*options)
1837 if ask_save_modifications(utf8(_("Save before quitting?")),
1838 utf8(_("Do you want to save your changes before quitting?")),
1844 def show_popup(parent, msg, *options)
1845 dialog = Gtk::Dialog.new
1846 if options[0] && options[0][:title]
1847 dialog.title = options[0][:title]
1849 dialog.title = utf8(_("Booh message"))
1851 lbl = Gtk::Label.new
1852 if options[0] && options[0][:nomarkup]
1857 if options[0] && options[0][:centered]
1858 lbl.set_justify(Gtk::Justification::CENTER)
1860 if options[0] && options[0][:selectable]
1861 lbl.selectable = true
1863 if options[0] && options[0][:topwidget]
1864 dialog.vbox.add(options[0][:topwidget])
1866 if options[0] && options[0][:scrolled]
1867 sw = Gtk::ScrolledWindow.new(nil, nil)
1868 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1869 sw.add_with_viewport(lbl)
1871 dialog.set_default_size(500, 600)
1873 dialog.vbox.add(lbl)
1874 dialog.set_default_size(200, 120)
1876 if options[0] && options[0][:okcancel]
1877 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1879 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1881 if options[0] && options[0][:pos_centered]
1882 dialog.window_position = Gtk::Window::POS_CENTER
1884 dialog.window_position = Gtk::Window::POS_MOUSE
1887 if options[0] && options[0][:linkurl]
1888 linkbut = Gtk::Button.new('')
1889 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1890 linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1891 linkbut.relief = Gtk::RELIEF_NONE
1892 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1893 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1894 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1899 if !options[0] || !options[0][:not_transient]
1900 dialog.transient_for = parent
1901 dialog.run { |response|
1903 if options[0] && options[0][:okcancel]
1904 return response == Gtk::Dialog::RESPONSE_OK
1908 dialog.signal_connect('response') { dialog.destroy }
1912 def backend_wait_message(parent, msg, infopipe_path, mode)
1914 w.set_transient_for(parent)
1917 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1918 vb.pack_start(Gtk::Label.new(msg), false, false)
1920 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1921 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1922 if mode != 'one dir scan'
1923 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1925 if mode == 'web-album'
1926 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1927 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1929 vb.pack_start(Gtk::HSeparator.new, false, false)
1931 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1932 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1933 vb.pack_end(bottom, false, false)
1935 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1936 refresh_thread = Thread.new {
1937 directories_counter = 0
1938 while line = infopipe.gets
1939 if line =~ /^directories: (\d+), sizes: (\d+)/
1940 directories = $1.to_f + 1
1942 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1943 elements = $3.to_f + 1
1944 if mode == 'web-album'
1948 gtk_thread_protect { pb1_1.fraction = 0 }
1949 if mode != 'one dir scan'
1950 newtext = utf8(full_src_dir_to_rel($1, $2))
1951 newtext = '/' if newtext == ''
1952 gtk_thread_protect { pb1_2.text = newtext }
1953 directories_counter += 1
1954 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1956 elsif line =~ /^processing element$/
1957 element_counter += 1
1958 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1959 elsif line =~ /^processing size$/
1960 element_counter += 1
1961 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1962 elsif line =~ /^finished processing sizes$/
1963 gtk_thread_protect { pb1_1.fraction = 1 }
1964 elsif line =~ /^creating index.html$/
1965 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1966 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1967 directories_counter = 0
1968 elsif line =~ /^index.html: (.+)\|(.+)/
1969 newtext = utf8(full_src_dir_to_rel($1, $2))
1970 newtext = '/' if newtext == ''
1971 gtk_thread_protect { pb2.text = newtext }
1972 directories_counter += 1
1973 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1974 elsif line =~ /^die: (.*)$/
1981 w.signal_connect('delete-event') { w.destroy }
1982 w.signal_connect('destroy') {
1983 Thread.kill(refresh_thread)
1984 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1987 system("rm -f #{infopipe_path}")
1990 w.window_position = Gtk::Window::POS_CENTER
1996 def call_backend(cmd, waitmsg, mode, params)
1997 pipe = Tempfile.new("boohpipe")
1999 system("mkfifo #{pipe.path}")
2000 cmd += " --info-pipe #{pipe.path}"
2001 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2006 id, exitstatus = Process.waitpid2(pid)
2007 gtk_thread_protect { w8.destroy }
2009 if params[:successmsg]
2010 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2012 if params[:closure_after]
2013 gtk_thread_protect(¶ms[:closure_after])
2015 elsif exitstatus == 15
2016 #- say nothing, user aborted
2018 gtk_thread_protect { show_popup($main_window,
2019 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2025 button.signal_connect('clicked') {
2026 Process.kill('SIGTERM', pid)
2030 def save_changes(*forced)
2031 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2035 $xmldir.delete_attribute('already-generated')
2037 propagate_children = proc { |xmldir|
2038 if xmldir.attributes['subdirs-caption']
2039 xmldir.delete_attribute('already-generated')
2041 xmldir.elements.each('dir') { |element|
2042 propagate_children.call(element)
2046 if $xmldir.child_byname_notattr('dir', 'deleted')
2047 new_title = $subalbums_title.buffer.text
2048 if new_title != $xmldir.attributes['subdirs-caption']
2049 parent = $xmldir.parent
2050 if parent.name == 'dir'
2051 parent.delete_attribute('already-generated')
2053 propagate_children.call($xmldir)
2055 $xmldir.add_attribute('subdirs-caption', new_title)
2056 $xmldir.elements.each('dir') { |element|
2057 if !element.attributes['deleted']
2058 path = element.attributes['path']
2059 newtext = $subalbums_edits[path][:editzone].buffer.text
2060 if element.attributes['subdirs-caption']
2061 if element.attributes['subdirs-caption'] != newtext
2062 propagate_children.call(element)
2064 element.add_attribute('subdirs-caption', newtext)
2065 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2067 if element.attributes['thumbnails-caption'] != newtext
2068 element.delete_attribute('already-generated')
2070 element.add_attribute('thumbnails-caption', newtext)
2071 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2077 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2078 if $xmldir.attributes['thumbnails-caption']
2079 path = $xmldir.attributes['path']
2080 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2082 elsif $xmldir.attributes['thumbnails-caption']
2083 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2086 #- remove and reinsert elements to reflect new ordering
2089 $xmldir.elements.each { |element|
2090 if element.name == 'image' || element.name == 'video'
2091 saves[element.attributes['filename']] = element.remove
2095 $autotable.current_order.each { |path|
2096 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2097 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2100 saves.each_key { |path|
2101 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2102 chld.add_attribute('deleted', 'true')
2106 def sort_by_exif_date
2110 $xmldir.elements.each { |element|
2111 if element.name == 'image' || element.name == 'video'
2112 current_order << element.attributes['filename']
2116 #- look for EXIF dates
2118 w.set_transient_for($main_window)
2120 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2121 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2122 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2123 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2124 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2125 vb.pack_end(bottom, false, false)
2127 w.signal_connect('delete-event') { w.destroy }
2128 w.window_position = Gtk::Window::POS_CENTER
2132 b.signal_connect('clicked') { aborted = true }
2135 current_order.each { |f|
2137 if entry2type(f) == 'image'
2139 pb.fraction = i.to_f / current_order.size
2140 Gtk.main_iteration while Gtk.events_pending?
2141 date_time = `identify -format "%[EXIF:DateTime]" '#{from_utf8($current_path + "/" + f)}'`.chomp
2142 if $? == 0 && date_time != ''
2143 dates[f] = date_time
2156 $xmldir.elements.each { |element|
2157 if element.name == 'image' || element.name == 'video'
2158 saves[element.attributes['filename']] = element.remove
2162 #- find a good fallback for all entries without a date (still next to the item they were next to)
2163 neworder = dates.keys.sort { |a,b| dates[a] <=> dates[b] }
2164 for i in 0 .. current_order.size - 1
2165 if ! neworder.include?(current_order[i])
2167 while j > 0 && ! neworder.include?(current_order[j])
2170 neworder[(neworder.index(current_order[j]) || -1 ) + 1, 0] = current_order[i]
2174 $xmldir.add_element(saves[f].name, saves[f].attributes)
2177 #- let the auto-table reflect new ordering
2181 def remove_all_captions
2184 $autotable.current_order.each { |path|
2185 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2186 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2188 save_undo(_("remove all captions"),
2190 texts.each_key { |key|
2191 $name2widgets[key][:textview].buffer.text = texts[key]
2193 $notebook.set_page(1)
2195 texts.each_key { |key|
2196 $name2widgets[key][:textview].buffer.text = ''
2198 $notebook.set_page(1)
2204 $selected_elements.each_key { |path|
2205 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2211 $selected_elements = {}
2215 $undo_tb.sensitive = $undo_mb.sensitive = false
2216 $redo_tb.sensitive = $redo_mb.sensitive = false
2222 $subalbums_vb.children.each { |chld|
2223 $subalbums_vb.remove(chld)
2225 $subalbums = Gtk::Table.new(0, 0, true)
2226 current_y_sub_albums = 0
2228 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2229 $subalbums_edits = {}
2230 subalbums_counter = 0
2231 subalbums_edits_bypos = {}
2233 add_subalbum = proc { |xmldir, counter|
2234 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2235 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2236 if xmldir == $xmldir
2237 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2238 caption = xmldir.attributes['thumbnails-caption']
2239 captionfile, dummy = find_subalbum_caption_info(xmldir)
2240 infotype = 'thumbnails'
2242 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2243 captionfile, caption = find_subalbum_caption_info(xmldir)
2244 infotype = find_subalbum_info_type(xmldir)
2246 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2247 hbox = Gtk::HBox.new
2248 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2250 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2253 my_gen_real_thumbnail = proc {
2254 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2257 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2258 f.add(img = Gtk::Image.new)
2259 my_gen_real_thumbnail.call
2261 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2263 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2264 $subalbums.attach(hbox,
2265 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2267 frame, textview = create_editzone($subalbums_sw, 0, img)
2268 textview.buffer.text = caption
2269 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2270 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2272 change_image = proc {
2273 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2275 Gtk::FileChooser::ACTION_OPEN,
2277 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2278 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2279 fc.transient_for = $main_window
2280 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))
2281 f.add(preview_img = Gtk::Image.new)
2283 fc.signal_connect('update-preview') { |w|
2285 if fc.preview_filename
2286 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2287 fc.preview_widget_active = true
2289 rescue Gdk::PixbufError
2290 fc.preview_widget_active = false
2293 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2295 old_file = captionfile
2296 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2297 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2298 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2299 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2301 new_file = fc.filename
2302 msg 3, "new captionfile is: #{fc.filename}"
2303 perform_changefile = proc {
2304 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2305 $modified_pixbufs.delete(thumbnail_file)
2306 xmldir.delete_attribute("#{infotype}-rotate")
2307 xmldir.delete_attribute("#{infotype}-color-swap")
2308 xmldir.delete_attribute("#{infotype}-enhance")
2309 xmldir.delete_attribute("#{infotype}-frame-offset")
2310 my_gen_real_thumbnail.call
2312 perform_changefile.call
2314 save_undo(_("change caption file for sub-album"),
2316 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2317 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2318 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2319 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2320 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2321 my_gen_real_thumbnail.call
2322 $notebook.set_page(0)
2324 perform_changefile.call
2325 $notebook.set_page(0)
2333 system("rm -f '#{thumbnail_file}'")
2334 my_gen_real_thumbnail.call
2337 rotate_and_cleanup = proc { |angle|
2338 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2339 system("rm -f '#{thumbnail_file}'")
2342 move = proc { |direction|
2345 save_changes('forced')
2346 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2347 if direction == 'up'
2348 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2349 subalbums_edits_bypos[oldpos - 1][:position] += 1
2351 if direction == 'down'
2352 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2353 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2355 if direction == 'top'
2356 for i in 1 .. oldpos - 1
2357 subalbums_edits_bypos[i][:position] += 1
2359 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2361 if direction == 'bottom'
2362 for i in oldpos + 1 .. subalbums_counter
2363 subalbums_edits_bypos[i][:position] -= 1
2365 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2369 $xmldir.elements.each('dir') { |element|
2370 if (!element.attributes['deleted'])
2371 elems << [ element.attributes['path'], element.remove ]
2374 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2375 each { |e| $xmldir.add_element(e[1]) }
2376 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2377 $xmldir.elements.each('descendant::dir') { |elem|
2378 elem.delete_attribute('already-generated')
2381 sel = $albums_tv.selection.selected_rows
2383 populate_subalbums_treeview(false)
2384 $albums_tv.selection.select_path(sel[0])
2387 color_swap_and_cleanup = proc {
2388 perform_color_swap_and_cleanup = proc {
2389 color_swap(xmldir, "#{infotype}-")
2390 my_gen_real_thumbnail.call
2392 perform_color_swap_and_cleanup.call
2394 save_undo(_("color swap"),
2396 perform_color_swap_and_cleanup.call
2397 $notebook.set_page(0)
2399 perform_color_swap_and_cleanup.call
2400 $notebook.set_page(0)
2405 change_frame_offset_and_cleanup = proc {
2406 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2407 perform_change_frame_offset_and_cleanup = proc { |val|
2408 change_frame_offset(xmldir, "#{infotype}-", val)
2409 my_gen_real_thumbnail.call
2411 perform_change_frame_offset_and_cleanup.call(values[:new])
2413 save_undo(_("specify frame offset"),
2415 perform_change_frame_offset_and_cleanup.call(values[:old])
2416 $notebook.set_page(0)
2418 perform_change_frame_offset_and_cleanup.call(values[:new])
2419 $notebook.set_page(0)
2425 whitebalance_and_cleanup = proc {
2426 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2427 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2428 perform_change_whitebalance_and_cleanup = proc { |val|
2429 change_whitebalance(xmldir, "#{infotype}-", val)
2430 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2431 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2432 system("rm -f '#{thumbnail_file}'")
2434 perform_change_whitebalance_and_cleanup.call(values[:new])
2436 save_undo(_("fix white balance"),
2438 perform_change_whitebalance_and_cleanup.call(values[:old])
2439 $notebook.set_page(0)
2441 perform_change_whitebalance_and_cleanup.call(values[:new])
2442 $notebook.set_page(0)
2448 gammacorrect_and_cleanup = proc {
2449 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2450 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2451 perform_change_gammacorrect_and_cleanup = proc { |val|
2452 change_gammacorrect(xmldir, "#{infotype}-", val)
2453 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2454 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2455 system("rm -f '#{thumbnail_file}'")
2457 perform_change_gammacorrect_and_cleanup.call(values[:new])
2459 save_undo(_("gamma correction"),
2461 perform_change_gammacorrect_and_cleanup.call(values[:old])
2462 $notebook.set_page(0)
2464 perform_change_gammacorrect_and_cleanup.call(values[:new])
2465 $notebook.set_page(0)
2471 enhance_and_cleanup = proc {
2472 perform_enhance_and_cleanup = proc {
2473 enhance(xmldir, "#{infotype}-")
2474 my_gen_real_thumbnail.call
2477 perform_enhance_and_cleanup.call
2479 save_undo(_("enhance"),
2481 perform_enhance_and_cleanup.call
2482 $notebook.set_page(0)
2484 perform_enhance_and_cleanup.call
2485 $notebook.set_page(0)
2490 evtbox.signal_connect('button-press-event') { |w, event|
2491 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2493 rotate_and_cleanup.call(90)
2495 rotate_and_cleanup.call(-90)
2496 elsif $enhance.active?
2497 enhance_and_cleanup.call
2500 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2501 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2502 { :forbid_left => true, :forbid_right => true,
2503 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2504 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2505 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2506 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2507 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2509 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2514 evtbox.signal_connect('button-press-event') { |w, event|
2515 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2519 evtbox.signal_connect('button-release-event') { |w, event|
2520 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2521 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2522 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2523 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2524 msg 3, "gesture rotate: #{angle}"
2525 rotate_and_cleanup.call(angle)
2528 $gesture_press = nil
2531 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2532 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2533 current_y_sub_albums += 1
2536 if $xmldir.child_byname_notattr('dir', 'deleted')
2538 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2539 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2540 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2541 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2542 #- this album image/caption
2543 if $xmldir.attributes['thumbnails-caption']
2544 add_subalbum.call($xmldir, 0)
2547 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2548 $xmldir.elements.each { |element|
2549 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2550 #- element (image or video) of this album
2551 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2552 msg 3, "dest_img: #{dest_img}"
2553 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2554 total[element.name] += 1
2556 if element.name == 'dir' && !element.attributes['deleted']
2557 #- sub-album image/caption
2558 add_subalbum.call(element, subalbums_counter += 1)
2559 total[element.name] += 1
2562 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2563 total['image'], total['video'], total['dir'] ]))
2564 $subalbums_vb.add($subalbums)
2565 $subalbums_vb.show_all
2567 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2568 $notebook.get_tab_label($autotable_sw).sensitive = false
2569 $notebook.set_page(0)
2570 $thumbnails_title.buffer.text = ''
2572 $notebook.get_tab_label($autotable_sw).sensitive = true
2573 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2576 if !$xmldir.child_byname_notattr('dir', 'deleted')
2577 $notebook.get_tab_label($subalbums_sw).sensitive = false
2578 $notebook.set_page(1)
2580 $notebook.get_tab_label($subalbums_sw).sensitive = true
2584 def pixbuf_or_nil(filename)
2586 return Gdk::Pixbuf.new(filename)
2592 def theme_choose(current)
2593 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2595 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2596 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2597 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2599 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2600 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2601 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2602 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2603 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2604 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2605 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2606 treeview.signal_connect('button-press-event') { |w, event|
2607 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2608 dialog.response(Gtk::Dialog::RESPONSE_OK)
2612 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2614 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2617 iter[0] = File.basename(dir)
2618 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2619 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2620 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2621 if File.basename(dir) == current
2622 treeview.selection.select_iter(iter)
2626 dialog.set_default_size(700, 400)
2627 dialog.vbox.show_all
2628 dialog.run { |response|
2629 iter = treeview.selection.selected
2631 if response == Gtk::Dialog::RESPONSE_OK && iter
2632 return model.get_value(iter, 0)
2638 def show_password_protections
2639 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2640 child_iter = $albums_iters[xmldir.attributes['path']]
2641 if xmldir.attributes['password-protect']
2642 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2643 already_protected = true
2644 elsif already_protected
2645 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2647 pix = pix.saturate_and_pixelate(1, true)
2653 xmldir.elements.each('dir') { |elem|
2654 if !elem.attributes['deleted']
2655 examine_dir_elem.call(child_iter, elem, already_protected)
2659 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2662 def populate_subalbums_treeview(select_first)
2666 $subalbums_vb.children.each { |chld|
2667 $subalbums_vb.remove(chld)
2670 source = $xmldoc.root.attributes['source']
2671 msg 3, "source: #{source}"
2673 xmldir = $xmldoc.elements['//dir']
2674 if !xmldir || xmldir.attributes['path'] != source
2675 msg 1, _("Corrupted booh file...")
2679 append_dir_elem = proc { |parent_iter, xmldir|
2680 child_iter = $albums_ts.append(parent_iter)
2681 child_iter[0] = File.basename(xmldir.attributes['path'])
2682 child_iter[1] = xmldir.attributes['path']
2683 $albums_iters[xmldir.attributes['path']] = child_iter
2684 msg 3, "puttin location: #{xmldir.attributes['path']}"
2685 xmldir.elements.each('dir') { |elem|
2686 if !elem.attributes['deleted']
2687 append_dir_elem.call(child_iter, elem)
2691 append_dir_elem.call(nil, xmldir)
2692 show_password_protections
2694 $albums_tv.expand_all
2696 $albums_tv.selection.select_iter($albums_ts.iter_first)
2700 def open_file(filename)
2704 $current_path = nil #- invalidate
2705 $modified_pixbufs = {}
2708 $subalbums_vb.children.each { |chld|
2709 $subalbums_vb.remove(chld)
2712 if !File.exists?(filename)
2713 return utf8(_("File not found."))
2717 $xmldoc = REXML::Document.new File.new(filename)
2722 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2723 if entry2type(filename).nil?
2724 return utf8(_("Not a booh file!"))
2726 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."))
2730 if !source = $xmldoc.root.attributes['source']
2731 return utf8(_("Corrupted booh file..."))
2734 if !dest = $xmldoc.root.attributes['destination']
2735 return utf8(_("Corrupted booh file..."))
2738 if !theme = $xmldoc.root.attributes['theme']
2739 return utf8(_("Corrupted booh file..."))
2742 if $xmldoc.root.attributes['version'] < '0.8.4'
2743 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2744 mark_document_as_dirty
2745 if $xmldoc.root.attributes['version'] < '0.8.4'
2746 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2747 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2748 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2749 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2750 if old_dest_dir != new_dest_dir
2751 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2753 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2754 xmldir.elements.each { |element|
2755 if %w(image video).include?(element.name) && !element.attributes['deleted']
2756 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2757 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2758 Dir[old_name + '*'].each { |file|
2759 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2760 file != new_file and sys("mv '#{file}' '#{new_file}'")
2763 if element.name == 'dir' && !element.attributes['deleted']
2764 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2765 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2766 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2770 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2774 $xmldoc.root.add_attribute('version', $VERSION)
2777 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2778 optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2779 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2781 $filename = filename
2782 select_theme(theme, limit_sizes, optimizefor32, nperrow)
2783 $default_size['thumbnails'] =~ /(.*)x(.*)/
2784 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2785 $albums_thumbnail_size =~ /(.*)x(.*)/
2786 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2788 populate_subalbums_treeview(true)
2790 $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
2794 def open_file_user(filename)
2795 result = open_file(filename)
2797 $config['last-opens'] ||= []
2798 if $config['last-opens'][-1] != utf8(filename)
2799 $config['last-opens'] << utf8(filename)
2801 $orig_filename = $filename
2802 tmp = Tempfile.new("boohtemp")
2805 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2807 $tempfiles << $filename << "#{$filename}.backup"
2809 $orig_filename = nil
2815 if !ask_save_modifications(utf8(_("Save this album?")),
2816 utf8(_("Do you want to save the changes to this album?")),
2817 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2820 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2822 Gtk::FileChooser::ACTION_OPEN,
2824 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2825 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2826 fc.set_current_folder(File.expand_path("~/.booh"))
2827 fc.transient_for = $main_window
2830 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2831 push_mousecursor_wait(fc)
2832 msg = open_file_user(fc.filename)
2848 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2853 def additional_booh_options
2856 options += "--mproc #{$config['mproc'].to_i} "
2858 options += "--comments-format '#{$config['comments-format']}'"
2863 if !ask_save_modifications(utf8(_("Save this album?")),
2864 utf8(_("Do you want to save the changes to this album?")),
2865 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2868 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2870 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2871 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2872 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2874 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2875 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2876 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2877 tbl.attach(src = Gtk::Entry.new,
2878 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2879 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2880 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2881 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2882 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2883 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2884 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2885 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2886 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2887 tbl.attach(dest = Gtk::Entry.new,
2888 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2889 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2890 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2891 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2892 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2893 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2894 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2895 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2896 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2898 tooltips = Gtk::Tooltips.new
2899 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2900 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2901 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2902 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2903 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2904 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2905 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)
2906 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2907 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2908 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
2909 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
2910 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)
2911 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2912 pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
2913 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)
2915 src_nb_calculated_for = ''
2917 process_src_nb = proc {
2918 if src.text != src_nb_calculated_for
2919 src_nb_calculated_for = src.text
2921 Thread.kill(src_nb_thread)
2924 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
2925 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
2927 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2928 if File.readable?(from_utf8_safe(src_nb_calculated_for))
2929 src_nb_thread = Thread.new {
2930 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2931 total = { 'image' => 0, 'video' => 0, nil => 0 }
2932 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2933 if File.basename(dir) =~ /^\./
2937 Dir.entries(dir.chomp).each { |file|
2938 total[entry2type(file)] += 1
2940 rescue Errno::EACCES, Errno::ENOENT
2944 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2948 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2951 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2957 timeout_src_nb = Gtk.timeout_add(100) {
2961 src_browse.signal_connect('clicked') {
2962 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2964 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2966 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2967 fc.transient_for = $main_window
2968 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2969 src.text = utf8(fc.filename)
2971 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2976 dest_browse.signal_connect('clicked') {
2977 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2979 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2981 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2982 fc.transient_for = $main_window
2983 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2984 dest.text = utf8(fc.filename)
2989 conf_browse.signal_connect('clicked') {
2990 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2992 Gtk::FileChooser::ACTION_SAVE,
2994 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2995 fc.transient_for = $main_window
2996 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2997 fc.set_current_folder(File.expand_path("~/.booh"))
2998 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2999 conf.text = utf8(fc.filename)
3006 recreate_theme_config = proc {
3007 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3009 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3010 $images_size.each { |s|
3011 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3015 tooltips.set_tip(cb, utf8(s['description']), nil)
3016 theme_sizes << { :widget => cb, :value => s['name'] }
3018 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3019 tooltips = Gtk::Tooltips.new
3020 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3021 theme_sizes << { :widget => cb, :value => 'original' }
3024 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3027 $allowed_N_values.each { |n|
3029 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3031 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3033 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3037 nperrows << { :widget => rb, :value => n }
3039 nperrowradios.show_all
3041 recreate_theme_config.call
3043 theme_button.signal_connect('clicked') {
3044 if newtheme = theme_choose(theme_button.label)
3045 theme_button.label = newtheme
3046 recreate_theme_config.call
3050 dialog.vbox.add(frame1)
3051 dialog.vbox.add(frame2)
3052 dialog.window_position = Gtk::Window::POS_MOUSE
3058 dialog.run { |response|
3059 if response == Gtk::Dialog::RESPONSE_OK
3060 srcdir = from_utf8_safe(src.text)
3061 destdir = from_utf8_safe(dest.text)
3062 confpath = from_utf8_safe(conf.text)
3063 if src.text != '' && srcdir == ''
3064 show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3066 elsif !File.directory?(srcdir)
3067 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3069 elsif dest.text != '' && destdir == ''
3070 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3072 elsif destdir != make_dest_filename(destdir)
3073 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3075 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3076 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
3078 elsif File.exists?(destdir) && !File.directory?(destdir)
3079 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3081 elsif conf.text == ''
3082 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3084 elsif conf.text != '' && confpath == ''
3085 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3087 elsif File.directory?(confpath)
3088 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3090 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3091 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3093 system("mkdir '#{destdir}'")
3094 if !File.directory?(destdir)
3095 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3107 srcdir = from_utf8(src.text)
3108 destdir = from_utf8(dest.text)
3109 configskel = File.expand_path(from_utf8(conf.text))
3110 theme = theme_button.label
3111 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3112 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3113 opt432 = optimize432.active?
3114 madewith = madewithentry.text
3115 indexlink = indexlinkentry.text
3118 Thread.kill(src_nb_thread)
3119 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3122 Gtk.timeout_remove(timeout_src_nb)
3125 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3126 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3127 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3128 utf8(_("Please wait while scanning source directory...")),
3130 { :closure_after => proc { open_file_user(configskel) } })
3135 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3137 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3138 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3139 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3141 source = $xmldoc.root.attributes['source']
3142 dest = $xmldoc.root.attributes['destination']
3143 theme = $xmldoc.root.attributes['theme']
3144 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3145 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3146 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3148 limit_sizes = limit_sizes.split(/,/)
3150 madewith = $xmldoc.root.attributes['made-with']
3151 indexlink = $xmldoc.root.attributes['index-link']
3153 tooltips = Gtk::Tooltips.new
3154 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3155 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3156 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3157 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3158 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3159 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3160 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3161 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3162 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3163 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3164 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3165 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3166 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3168 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3169 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3170 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3171 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3172 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3173 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3174 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)
3175 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3176 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3178 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3179 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3181 indexlinkentry.text = indexlink
3183 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)
3184 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3185 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3187 madewithentry.text = madewith
3189 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)
3193 recreate_theme_config = proc {
3194 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3196 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3198 $images_size.each { |s|
3199 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3201 if limit_sizes.include?(s['name'])
3209 tooltips.set_tip(cb, utf8(s['description']), nil)
3210 theme_sizes << { :widget => cb, :value => s['name'] }
3212 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3213 tooltips = Gtk::Tooltips.new
3214 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3215 if limit_sizes && limit_sizes.include?('original')
3218 theme_sizes << { :widget => cb, :value => 'original' }
3221 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3224 $allowed_N_values.each { |n|
3226 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3228 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3230 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3231 nperrowradios.add(Gtk::Label.new(' '))
3232 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3235 nperrows << { :widget => rb, :value => n.to_s }
3237 nperrowradios.show_all
3239 recreate_theme_config.call
3241 theme_button.signal_connect('clicked') {
3242 if newtheme = theme_choose(theme_button.label)
3245 theme_button.label = newtheme
3246 recreate_theme_config.call
3250 dialog.vbox.add(frame1)
3251 dialog.vbox.add(frame2)
3252 dialog.window_position = Gtk::Window::POS_MOUSE
3258 dialog.run { |response|
3259 if response == Gtk::Dialog::RESPONSE_OK
3260 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3261 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3270 save_theme = theme_button.label
3271 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3272 save_opt432 = optimize432.active?
3273 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3274 save_madewith = madewithentry.text
3275 save_indexlink = indexlinkentry.text
3278 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith || save_indexlink != indexlinkentry)
3279 mark_document_as_dirty
3281 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3282 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3283 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3284 utf8(_("Please wait while scanning source directory...")),
3286 { :closure_after => proc {
3287 open_file($filename)
3296 sel = $albums_tv.selection.selected_rows
3298 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3299 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3300 utf8(_("Please wait while scanning source directory...")),
3302 { :closure_after => proc {
3303 open_file($filename)
3304 $albums_tv.selection.select_path(sel[0])
3312 sel = $albums_tv.selection.selected_rows
3314 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3315 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3316 utf8(_("Please wait while scanning source directory...")),
3318 { :closure_after => proc {
3319 open_file($filename)
3320 $albums_tv.selection.select_path(sel[0])
3328 theme = $xmldoc.root.attributes['theme']
3329 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3331 limit_sizes = "--sizes #{limit_sizes}"
3333 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3334 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3335 utf8(_("Please wait while scanning source directory...")),
3337 { :closure_after => proc {
3338 open_file($filename)
3344 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3346 Gtk::FileChooser::ACTION_SAVE,
3348 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3349 fc.transient_for = $main_window
3350 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3351 fc.set_current_folder(File.expand_path("~/.booh"))
3352 fc.filename = $orig_filename
3353 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3354 $orig_filename = fc.filename
3355 if ! save_current_file_user
3359 $config['last-opens'] ||= []
3360 $config['last-opens'] << $orig_filename
3366 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3368 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3369 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3370 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3372 dialog.vbox.add(notebook = Gtk::Notebook.new)
3373 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3374 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3375 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3376 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)),
3377 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3378 tooltips = Gtk::Tooltips.new
3379 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3380 for example: /usr/bin/mplayer %f")), nil)
3381 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3382 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3383 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3384 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3385 tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3386 for example: /usr/bin/gimp-remote %f")), nil)
3387 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3388 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3389 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3390 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3391 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3392 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3393 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3394 0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3395 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)),
3396 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3397 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)
3398 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3399 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3400 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)
3401 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3402 0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3403 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)
3405 smp_check.signal_connect('toggled') {
3406 if smp_check.active?
3407 smp_hbox.sensitive = true
3409 smp_hbox.sensitive = false
3413 smp_check.active = true
3414 smp_spin.value = $config['mproc'].to_i
3416 nogestures_check.active = $config['nogestures']
3417 deleteondisk_check.active = $config['deleteondisk']
3419 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3420 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3421 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3422 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3423 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3424 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3425 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3426 tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3427 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3428 tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3429 2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3430 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)
3431 commentsformat_help.signal_connect('clicked') {
3432 show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3433 hence you should look at ImageMagick/identify documentation for the most
3434 accurate and up-to-date documentation. Last time I checked, documentation
3437 Print information about the image in a format of your choosing. You can
3438 include the image filename, type, width, height, Exif data, or other image
3439 attributes by embedding special format characters:
3442 %P page width and height
3446 %e filename extension
3451 %k number of unique colors
3458 %r image class and colorspace
3461 %u unique temporary filename
3474 displays MIFF:bird.miff 512x480 for an image titled bird.miff and whose
3475 width is 512 and height is 480.
3477 If the first character of string is @, the format is read from a file titled
3478 by the remaining characters in the string.
3480 You can also use the following special formatting syntax to print Exif
3481 information contained in the file:
3485 Where tag can be one of the following:
3487 * (print all Exif tags, in keyword=data format)
3488 ! (print all Exif tags, in tag_number data format)
3489 #hhhh (print data for Exif tag #hhhh)
3494 PhotometricInterpretation
3514 PrimaryChromaticities
3517 JPEGInterchangeFormat
3518 JPEGInterchangeFormatLength
3540 ComponentsConfiguration
3541 CompressedBitsPerPixel
3561 InteroperabilityOffset
3563 SpatialFrequencyResponse
3564 FocalPlaneXResolution
3565 FocalPlaneYResolution
3566 FocalPlaneResolutionUnit
3571 SceneType")), { :scrolled => true })
3574 dialog.vbox.show_all
3575 dialog.run { |response|
3576 if response == Gtk::Dialog::RESPONSE_OK
3577 $config['video-viewer'] = from_utf8(video_viewer_entry.text)
3578 $config['image-editor'] = from_utf8(image_editor_entry.text)
3579 $config['browser'] = from_utf8(browser_entry.text)
3580 if smp_check.active?
3581 $config['mproc'] = smp_spin.value.to_i
3583 $config.delete('mproc')
3585 $config['nogestures'] = nogestures_check.active?
3586 $config['deleteondisk'] = deleteondisk_check.active?
3588 $config['convert-enhance'] = from_utf8(enhance_entry.text)
3589 $config['comments-format'] = from_utf8(commentsformat_entry.text.gsub(/'/, ''))
3596 if $undo_tb.sensitive?
3597 $redo_tb.sensitive = $redo_mb.sensitive = true
3598 if not more_undoes = UndoHandler.undo($statusbar)
3599 $undo_tb.sensitive = $undo_mb.sensitive = false
3605 if $redo_tb.sensitive?
3606 $undo_tb.sensitive = $undo_mb.sensitive = true
3607 if not more_redoes = UndoHandler.redo($statusbar)
3608 $redo_tb.sensitive = $redo_mb.sensitive = false
3613 def show_one_click_explanation(intro)
3614 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
3616 %s When such a tool is activated
3617 (<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
3618 on a thumbnail will immediately apply the desired action.
3620 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
3621 ") % intro), { :pos_centered => true })
3626 GNU GENERAL PUBLIC LICENSE
3627 Version 2, June 1991
3629 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
3630 675 Mass Ave, Cambridge, MA 02139, USA
3631 Everyone is permitted to copy and distribute verbatim copies
3632 of this license document, but changing it is not allowed.
3636 The licenses for most software are designed to take away your
3637 freedom to share and change it. By contrast, the GNU General Public
3638 License is intended to guarantee your freedom to share and change free
3639 software--to make sure the software is free for all its users. This
3640 General Public License applies to most of the Free Software
3641 Foundation's software and to any other program whose authors commit to
3642 using it. (Some other Free Software Foundation software is covered by
3643 the GNU Library General Public License instead.) You can apply it to
3646 When we speak of free software, we are referring to freedom, not
3647 price. Our General Public Licenses are designed to make sure that you
3648 have the freedom to distribute copies of free software (and charge for
3649 this service if you wish), that you receive source code or can get it
3650 if you want it, that you can change the software or use pieces of it
3651 in new free programs; and that you know you can do these things.
3653 To protect your rights, we need to make restrictions that forbid
3654 anyone to deny you these rights or to ask you to surrender the rights.
3655 These restrictions translate to certain responsibilities for you if you
3656 distribute copies of the software, or if you modify it.
3658 For example, if you distribute copies of such a program, whether
3659 gratis or for a fee, you must give the recipients all the rights that
3660 you have. You must make sure that they, too, receive or can get the
3661 source code. And you must show them these terms so they know their
3664 We protect your rights with two steps: (1) copyright the software, and
3665 (2) offer you this license which gives you legal permission to copy,
3666 distribute and/or modify the software.
3668 Also, for each author's protection and ours, we want to make certain
3669 that everyone understands that there is no warranty for this free
3670 software. If the software is modified by someone else and passed on, we
3671 want its recipients to know that what they have is not the original, so
3672 that any problems introduced by others will not reflect on the original
3673 authors' reputations.
3675 Finally, any free program is threatened constantly by software
3676 patents. We wish to avoid the danger that redistributors of a free
3677 program will individually obtain patent licenses, in effect making the
3678 program proprietary. To prevent this, we have made it clear that any
3679 patent must be licensed for everyone's free use or not licensed at all.
3681 The precise terms and conditions for copying, distribution and
3682 modification follow.
3685 GNU GENERAL PUBLIC LICENSE
3686 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
3688 0. This License applies to any program or other work which contains
3689 a notice placed by the copyright holder saying it may be distributed
3690 under the terms of this General Public License. The "Program", below,
3691 refers to any such program or work, and a "work based on the Program"
3692 means either the Program or any derivative work under copyright law:
3693 that is to say, a work containing the Program or a portion of it,
3694 either verbatim or with modifications and/or translated into another
3695 language. (Hereinafter, translation is included without limitation in
3696 the term "modification".) Each licensee is addressed as "you".
3698 Activities other than copying, distribution and modification are not
3699 covered by this License; they are outside its scope. The act of
3700 running the Program is not restricted, and the output from the Program
3701 is covered only if its contents constitute a work based on the
3702 Program (independent of having been made by running the Program).
3703 Whether that is true depends on what the Program does.
3705 1. You may copy and distribute verbatim copies of the Program's
3706 source code as you receive it, in any medium, provided that you
3707 conspicuously and appropriately publish on each copy an appropriate
3708 copyright notice and disclaimer of warranty; keep intact all the
3709 notices that refer to this License and to the absence of any warranty;
3710 and give any other recipients of the Program a copy of this License
3711 along with the Program.
3713 You may charge a fee for the physical act of transferring a copy, and
3714 you may at your option offer warranty protection in exchange for a fee.
3716 2. You may modify your copy or copies of the Program or any portion
3717 of it, thus forming a work based on the Program, and copy and
3718 distribute such modifications or work under the terms of Section 1
3719 above, provided that you also meet all of these conditions:
3721 a) You must cause the modified files to carry prominent notices
3722 stating that you changed the files and the date of any change.
3724 b) You must cause any work that you distribute or publish, that in
3725 whole or in part contains or is derived from the Program or any
3726 part thereof, to be licensed as a whole at no charge to all third
3727 parties under the terms of this License.
3729 c) If the modified program normally reads commands interactively
3730 when run, you must cause it, when started running for such
3731 interactive use in the most ordinary way, to print or display an
3732 announcement including an appropriate copyright notice and a
3733 notice that there is no warranty (or else, saying that you provide
3734 a warranty) and that users may redistribute the program under
3735 these conditions, and telling the user how to view a copy of this
3736 License. (Exception: if the Program itself is interactive but
3737 does not normally print such an announcement, your work based on
3738 the Program is not required to print an announcement.)
3741 These requirements apply to the modified work as a whole. If
3742 identifiable sections of that work are not derived from the Program,
3743 and can be reasonably considered independent and separate works in
3744 themselves, then this License, and its terms, do not apply to those
3745 sections when you distribute them as separate works. But when you
3746 distribute the same sections as part of a whole which is a work based
3747 on the Program, the distribution of the whole must be on the terms of
3748 this License, whose permissions for other licensees extend to the
3749 entire whole, and thus to each and every part regardless of who wrote it.
3751 Thus, it is not the intent of this section to claim rights or contest
3752 your rights to work written entirely by you; rather, the intent is to
3753 exercise the right to control the distribution of derivative or
3754 collective works based on the Program.
3756 In addition, mere aggregation of another work not based on the Program
3757 with the Program (or with a work based on the Program) on a volume of
3758 a storage or distribution medium does not bring the other work under
3759 the scope of this License.
3761 3. You may copy and distribute the Program (or a work based on it,
3762 under Section 2) in object code or executable form under the terms of
3763 Sections 1 and 2 above provided that you also do one of the following:
3765 a) Accompany it with the complete corresponding machine-readable
3766 source code, which must be distributed under the terms of Sections
3767 1 and 2 above on a medium customarily used for software interchange; or,
3769 b) Accompany it with a written offer, valid for at least three
3770 years, to give any third party, for a charge no more than your
3771 cost of physically performing source distribution, a complete
3772 machine-readable copy of the corresponding source code, to be
3773 distributed under the terms of Sections 1 and 2 above on a medium
3774 customarily used for software interchange; or,
3776 c) Accompany it with the information you received as to the offer
3777 to distribute corresponding source code. (This alternative is
3778 allowed only for noncommercial distribution and only if you
3779 received the program in object code or executable form with such
3780 an offer, in accord with Subsection b above.)
3782 The source code for a work means the preferred form of the work for
3783 making modifications to it. For an executable work, complete source
3784 code means all the source code for all modules it contains, plus any
3785 associated interface definition files, plus the scripts used to
3786 control compilation and installation of the executable. However, as a
3787 special exception, the source code distributed need not include
3788 anything that is normally distributed (in either source or binary
3789 form) with the major components (compiler, kernel, and so on) of the
3790 operating system on which the executable runs, unless that component
3791 itself accompanies the executable.
3793 If distribution of executable or object code is made by offering
3794 access to copy from a designated place, then offering equivalent
3795 access to copy the source code from the same place counts as
3796 distribution of the source code, even though third parties are not
3797 compelled to copy the source along with the object code.
3800 4. You may not copy, modify, sublicense, or distribute the Program
3801 except as expressly provided under this License. Any attempt
3802 otherwise to copy, modify, sublicense or distribute the Program is
3803 void, and will automatically terminate your rights under this License.
3804 However, parties who have received copies, or rights, from you under
3805 this License will not have their licenses terminated so long as such
3806 parties remain in full compliance.
3808 5. You are not required to accept this License, since you have not
3809 signed it. However, nothing else grants you permission to modify or
3810 distribute the Program or its derivative works. These actions are
3811 prohibited by law if you do not accept this License. Therefore, by
3812 modifying or distributing the Program (or any work based on the
3813 Program), you indicate your acceptance of this License to do so, and
3814 all its terms and conditions for copying, distributing or modifying
3815 the Program or works based on it.
3817 6. Each time you redistribute the Program (or any work based on the
3818 Program), the recipient automatically receives a license from the
3819 original licensor to copy, distribute or modify the Program subject to
3820 these terms and conditions. You may not impose any further
3821 restrictions on the recipients' exercise of the rights granted herein.
3822 You are not responsible for enforcing compliance by third parties to
3825 7. If, as a consequence of a court judgment or allegation of patent
3826 infringement or for any other reason (not limited to patent issues),
3827 conditions are imposed on you (whether by court order, agreement or
3828 otherwise) that contradict the conditions of this License, they do not
3829 excuse you from the conditions of this License. If you cannot
3830 distribute so as to satisfy simultaneously your obligations under this
3831 License and any other pertinent obligations, then as a consequence you
3832 may not distribute the Program at all. For example, if a patent
3833 license would not permit royalty-free redistribution of the Program by
3834 all those who receive copies directly or indirectly through you, then
3835 the only way you could satisfy both it and this License would be to
3836 refrain entirely from distribution of the Program.
3838 If any portion of this section is held invalid or unenforceable under
3839 any particular circumstance, the balance of the section is intended to
3840 apply and the section as a whole is intended to apply in other
3843 It is not the purpose of this section to induce you to infringe any
3844 patents or other property right claims or to contest validity of any
3845 such claims; this section has the sole purpose of protecting the
3846 integrity of the free software distribution system, which is
3847 implemented by public license practices. Many people have made
3848 generous contributions to the wide range of software distributed
3849 through that system in reliance on consistent application of that
3850 system; it is up to the author/donor to decide if he or she is willing
3851 to distribute software through any other system and a licensee cannot
3854 This section is intended to make thoroughly clear what is believed to
3855 be a consequence of the rest of this License.
3858 8. If the distribution and/or use of the Program is restricted in
3859 certain countries either by patents or by copyrighted interfaces, the
3860 original copyright holder who places the Program under this License
3861 may add an explicit geographical distribution limitation excluding
3862 those countries, so that distribution is permitted only in or among
3863 countries not thus excluded. In such case, this License incorporates
3864 the limitation as if written in the body of this License.
3866 9. The Free Software Foundation may publish revised and/or new versions
3867 of the General Public License from time to time. Such new versions will
3868 be similar in spirit to the present version, but may differ in detail to
3869 address new problems or concerns.
3871 Each version is given a distinguishing version number. If the Program
3872 specifies a version number of this License which applies to it and "any
3873 later version", you have the option of following the terms and conditions
3874 either of that version or of any later version published by the Free
3875 Software Foundation. If the Program does not specify a version number of
3876 this License, you may choose any version ever published by the Free Software
3879 10. If you wish to incorporate parts of the Program into other free
3880 programs whose distribution conditions are different, write to the author
3881 to ask for permission. For software which is copyrighted by the Free
3882 Software Foundation, write to the Free Software Foundation; we sometimes
3883 make exceptions for this. Our decision will be guided by the two goals
3884 of preserving the free status of all derivatives of our free software and
3885 of promoting the sharing and reuse of software generally.
3889 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
3890 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
3891 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
3892 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
3893 OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
3894 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
3895 TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
3896 PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
3897 REPAIR OR CORRECTION.
3899 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
3900 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
3901 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
3902 INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
3903 OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
3904 TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
3905 YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
3906 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
3907 POSSIBILITY OF SUCH DAMAGES.
3911 def create_menu_and_toolbar
3914 mb = Gtk::MenuBar.new
3916 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
3917 filesubmenu = Gtk::Menu.new
3918 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
3919 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
3920 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3921 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
3922 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
3923 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3924 tooltips = Gtk::Tooltips.new
3925 filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
3926 $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3927 tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
3928 filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
3929 $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3930 tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
3931 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
3932 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3933 tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums (anywhere)")), nil)
3934 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3935 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
3936 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3937 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
3938 filesubmenu.append($view_wa = Gtk::ImageMenuItem.new(utf8(_("View web-album with browser"))).set_sensitive(false))
3939 $view_wa.image = Gtk::Image.new("#{$FPATH}/images/stock-view-webalbum-16.png")
3940 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3941 filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
3942 tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
3943 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3944 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
3945 filemenu.set_submenu(filesubmenu)
3948 new.signal_connect('activate') { new_album }
3949 open.signal_connect('activate') { open_file_popup }
3950 $save.signal_connect('activate') { save_current_file_user }
3951 $save_as.signal_connect('activate') { save_as_do }
3952 $merge_current.signal_connect('activate') { merge_current }
3953 $merge_newsubs.signal_connect('activate') { merge_newsubs }
3954 $merge.signal_connect('activate') { merge }
3955 $generate.signal_connect('activate') {
3957 call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
3958 utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
3960 { :successmsg => utf8(_("Your web-album is now ready in directory '%s'.
3961 Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
3962 :successmsg_linkurl => $xmldoc.root.attributes['destination'],
3963 :closure_after => proc {
3964 $xmldoc.elements.each('//dir') { |elem|
3965 $modified ||= elem.attributes['already-generated'].nil?
3966 elem.add_attribute('already-generated', 'true')
3968 UndoHandler.cleanup #- prevent save_changes to mark current dir as not already generated
3969 $undo_tb.sensitive = $undo_mb.sensitive = false
3970 $redo_tb.sensitive = $redo_mb.sensitive = false
3972 $generated_outofline = true
3975 $view_wa.signal_connect('activate') {
3976 indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
3977 if File.exists?(indexhtml)
3980 show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
3983 $properties.signal_connect('activate') { properties }
<