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['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
112 if !FileTest.directory?(File.expand_path('~/.booh'))
113 system("mkdir ~/.booh")
121 if !system("which convert >/dev/null 2>/dev/null")
122 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
123 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
126 if !system("which identify >/dev/null 2>/dev/null")
127 show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
128 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
130 missing = %w(transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
132 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
135 viewer_binary = $config['video-viewer'].split.first
136 if viewer_binary && !File.executable?(viewer_binary)
137 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
138 You should fix this in Edit/Preferences so that you can view videos.
140 Problem was: '%s' is not an executable file.
141 Hint: don't forget to specify the full path to the executable,
142 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
144 browser_binary = $config['browser'].split.first
145 if browser_binary && !File.executable?(browser_binary)
146 show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
147 You should fix this in Edit/Preferences so that you can open URLs.
149 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
154 if $config['last-opens'] && $config['last-opens'].size > 5
155 $config['last-opens'] = $config['last-opens'][-5, 5]
158 ios = File.open($config_file, "w")
159 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
160 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
161 $config.each_pair { |key, value|
162 elem = $xmldoc.root.add_element key
164 $config[key].each_pair { |subkey, subvalue|
165 subelem = elem.add_element subkey
166 subelem.add_text subvalue.to_s
168 elsif value.is_a? Array
169 elem.add_text value.join('~~~')
174 elem.add_text value.to_s
178 $xmldoc.write(ios, 0)
181 $tempfiles.each { |f|
186 def set_mousecursor(what, *widget)
187 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
188 if widget[0] && widget[0].window
189 widget[0].window.cursor = cursor
191 if $main_window && $main_window.window
192 $main_window.window.cursor = cursor
194 $current_cursor = what
196 def set_mousecursor_wait(*widget)
197 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
198 if Thread.current == Thread.main
199 Gtk.main_iteration while Gtk.events_pending?
202 def set_mousecursor_normal(*widget)
203 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
205 def push_mousecursor_wait(*widget)
206 if $current_cursor != Gdk::Cursor::WATCH
207 $save_cursor = $current_cursor
208 gtk_thread_protect { set_mousecursor_wait(*widget) }
211 def pop_mousecursor(*widget)
212 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
216 source = $xmldoc.root.attributes['source']
217 dest = $xmldoc.root.attributes['destination']
218 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
221 def full_src_dir_to_rel(path, source)
222 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
225 def build_full_dest_filename(filename)
226 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
229 def save_undo(name, closure, *params)
230 UndoHandler.save_undo(name, closure, [ *params ])
231 $undo_tb.sensitive = $undo_mb.sensitive = true
232 $redo_tb.sensitive = $redo_mb.sensitive = false
235 def view_element(filename, closures)
236 if entry2type(filename) == 'video'
237 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
243 w = Gtk::Window.new.set_title(filename)
245 msg 3, "filename: #{filename}"
246 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
247 #- typically this file won't exist in case of videos; try with the largest thumbnail around
248 if !File.exists?(dest_img)
249 if entry2type(filename) == 'video'
250 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
251 if not alternatives.empty?
252 dest_img = alternatives[-1]
255 push_mousecursor_wait
256 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
258 if !File.exists?(dest_img)
259 msg 2, _("Could not generate fullscreen thumbnail!")
264 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)))
265 evt.signal_connect('button-press-event') { |this, event|
266 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
267 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
269 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
271 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
272 delete_item.signal_connect('activate') {
274 closures[:delete].call
277 menu.popup(nil, nil, event.button, event.time)
280 evt.signal_connect('button-release-event') { |this, event|
282 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
283 msg 3, "gesture delete: click-drag right button to the bottom"
285 closures[:delete].call
286 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
290 tooltips = Gtk::Tooltips.new
291 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
293 w.signal_connect('key-press-event') { |w,event|
294 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
296 closures[:delete].call
300 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
301 b.signal_connect('clicked') { w.destroy }
304 vb.pack_start(evt, false, false)
305 vb.pack_end(bottom, false, false)
308 w.signal_connect('delete-event') { w.destroy }
309 w.window_position = Gtk::Window::POS_CENTER
313 def scroll_upper(scrolledwindow, ypos_top)
314 newval = scrolledwindow.vadjustment.value -
315 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
316 if newval < scrolledwindow.vadjustment.lower
317 newval = scrolledwindow.vadjustment.lower
319 scrolledwindow.vadjustment.value = newval
322 def scroll_lower(scrolledwindow, ypos_bottom)
323 newval = scrolledwindow.vadjustment.value +
324 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
325 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
326 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
328 scrolledwindow.vadjustment.value = newval
331 def autoscroll_if_needed(scrolledwindow, image, textview)
332 #- autoscroll if cursor or image is not visible, if possible
333 if image && image.window || textview.window
334 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
335 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
336 current_miny_visible = scrolledwindow.vadjustment.value
337 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
338 if ypos_top < current_miny_visible
339 scroll_upper(scrolledwindow, ypos_top)
340 elsif ypos_bottom > current_maxy_visible
341 scroll_lower(scrolledwindow, ypos_bottom)
346 def create_editzone(scrolledwindow, pagenum, image)
347 frame = Gtk::Frame.new
348 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
349 frame.set_shadow_type(Gtk::SHADOW_IN)
350 textview.signal_connect('key-press-event') { |w, event|
351 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
352 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
353 scrolledwindow.signal_emit('key-press-event', event)
355 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
356 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
357 if event.keyval == Gdk::Keyval::GDK_Up
358 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
359 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
361 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
364 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
365 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
367 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
373 textview.signal_connect('focus-in-event') { |w, event|
374 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
378 candidate_undo_text = nil
379 textview.signal_connect('focus-in-event') { |w, event|
380 candidate_undo_text = textview.buffer.text
383 textview.signal_connect('key-release-event') { |w, event|
384 if candidate_undo_text && candidate_undo_text != textview.buffer.text
386 save_undo(_("text edit"),
388 save_text = textview.buffer.text
389 textview.buffer.text = text
391 $notebook.set_page(pagenum)
393 textview.buffer.text = save_text
395 $notebook.set_page(pagenum)
397 }, candidate_undo_text)
398 candidate_undo_text = nil
401 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)
402 autoscroll_if_needed(scrolledwindow, image, textview)
407 return [ frame, textview ]
410 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
412 if !$modified_pixbufs[thumbnail_img]
413 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
414 elsif !$modified_pixbufs[thumbnail_img][:orig]
415 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
418 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
421 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
422 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
423 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
424 if pixbuf.height > desired_y
425 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
426 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
427 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
432 if $modified_pixbufs[thumbnail_img][:whitebalance]
433 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
436 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
439 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
442 #- update rotate attribute
443 xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
445 $modified_pixbufs[thumbnail_img] ||= {}
446 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
447 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
449 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
452 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
455 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
457 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
459 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
460 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
462 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
463 $notebook.set_page(0)
464 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
469 def color_swap(xmldir, attributes_prefix)
471 if xmldir.attributes["#{attributes_prefix}color-swap"]
472 xmldir.delete_attribute("#{attributes_prefix}color-swap")
474 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
478 def enhance(xmldir, attributes_prefix)
480 if xmldir.attributes["#{attributes_prefix}enhance"]
481 xmldir.delete_attribute("#{attributes_prefix}enhance")
483 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
487 def change_frame_offset(xmldir, attributes_prefix, value)
489 xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
492 def ask_new_frame_offset(xmldir, attributes_prefix)
494 value = xmldir.attributes["#{attributes_prefix}frame-offset"]
499 dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
501 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
502 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
503 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
507 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
508 from. There are approximately 25 frames per second in a video.
511 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
512 entry.signal_connect('key-press-event') { |w, event|
513 if event.keyval == Gdk::Keyval::GDK_Return
514 dialog.response(Gtk::Dialog::RESPONSE_OK)
516 elsif event.keyval == Gdk::Keyval::GDK_Escape
517 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
520 false #- propagate if needed
524 dialog.window_position = Gtk::Window::POS_MOUSE
527 dialog.run { |response|
530 if response == Gtk::Dialog::RESPONSE_OK
532 msg 3, "changing frame offset to #{newval}"
533 return { :old => value, :new => newval }
540 def change_pano_amount(xmldir, attributes_prefix, value)
543 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
545 xmldir.add_attribute("#{attributes_prefix}pano-amount", value)
549 def ask_new_pano_amount(xmldir, attributes_prefix)
551 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
556 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
558 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
559 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
560 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
564 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
565 of this panorama image compared to other regular images. For example, if the panorama
566 was taken out of four photos on one row, counting the necessary overlap, the width of
567 this panorama image should probably be roughly three times the width of regular images.
569 With this information, booh will be able to generate panorama thumbnails looking
573 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)")))).
574 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
575 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
576 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
577 dialog.window_position = Gtk::Window::POS_MOUSE
580 spin.value = value.to_f
587 dialog.run { |response|
591 newval = spin.value.to_f
594 if response == Gtk::Dialog::RESPONSE_OK
596 msg 3, "changing panorama amount to #{newval}"
597 return { :old => value, :new => newval }
604 def change_whitebalance(xmlelem, attributes_prefix, value)
606 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
609 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
611 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
612 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
613 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
614 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
615 destfile = "#{thumbnail_img}-orig-whitebalance.jpg"
616 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
617 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
618 $modified_pixbufs[thumbnail_img] ||= {}
619 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
620 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
621 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
624 $modified_pixbufs[thumbnail_img] ||= {}
625 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
627 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
630 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
631 #- init $modified_pixbufs correctly
632 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
634 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
636 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
638 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
639 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
640 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
644 _("You can fix the <b>white balance</b> of the image, if your image is too blue
645 or too yellow because your camera didn't detect the light correctly. Drag the
646 slider below the image to the left for more blue, to the right for more yellow.
650 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
652 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
654 dialog.window_position = Gtk::Window::POS_MOUSE
658 timeout = Gtk.timeout_add(100) {
659 if hs.value != lastval
662 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
668 dialog.run { |response|
669 Gtk.timeout_remove(timeout)
670 if response == Gtk::Dialog::RESPONSE_OK
672 newval = hs.value.to_s
673 msg 3, "changing white balance to #{newval}"
675 return { :old => value, :new => newval }
678 $modified_pixbufs[thumbnail_img] ||= {}
679 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
680 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
688 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
689 system("rm -f '#{destfile}'")
690 #- type can be 'element' or 'subdir'
692 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
694 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
698 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
700 push_mousecursor_wait
701 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
704 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
710 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
711 distribute_multiple_call = Proc.new { |action, arg|
712 $selected_elements.each_key { |path|
713 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
715 if possible_actions[:can_multiple] && $selected_elements.length > 0
716 UndoHandler.begin_batch
717 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
718 UndoHandler.end_batch
720 closures[action].call(arg)
722 $selected_elements = {}
725 if optionals.include?('change_image')
726 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
727 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
728 changeimg.signal_connect('activate') { closures[:change].call }
729 menu.append(Gtk::SeparatorMenuItem.new)
731 if !possible_actions[:can_multiple] || $selected_elements.length == 0
734 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
735 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
736 view.signal_connect('activate') { closures[:view].call }
738 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
739 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
740 view.signal_connect('activate') { closures[:view].call }
741 menu.append(Gtk::SeparatorMenuItem.new)
744 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
745 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
746 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
747 exif.signal_connect('activate') { show_popup($main_window,
748 utf8(`identify -format "%[EXIF:*]" #{fullpath}`.sub(/MakerNote.*\n/, '')),
749 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
750 menu.append(Gtk::SeparatorMenuItem.new)
753 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
754 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
755 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
756 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
757 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
758 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
759 if !possible_actions[:can_multiple] || $selected_elements.length == 0
760 menu.append(Gtk::SeparatorMenuItem.new)
761 if !possible_actions[:forbid_left]
762 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
763 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
764 moveleft.signal_connect('activate') { closures[:move].call('left') }
765 if !possible_actions[:can_left]
766 moveleft.sensitive = false
769 if !possible_actions[:forbid_right]
770 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
771 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
772 moveright.signal_connect('activate') { closures[:move].call('right') }
773 if !possible_actions[:can_right]
774 moveright.sensitive = false
777 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
778 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
779 moveup.signal_connect('activate') { closures[:move].call('up') }
780 if !possible_actions[:can_up]
781 moveup.sensitive = false
783 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
784 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
785 movedown.signal_connect('activate') { closures[:move].call('down') }
786 if !possible_actions[:can_down]
787 movedown.sensitive = false
791 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
792 menu.append(Gtk::SeparatorMenuItem.new)
793 menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
794 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
795 color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
796 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
797 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
798 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
799 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
800 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
801 frame_offset.signal_connect('activate') {
802 if possible_actions[:can_multiple] && $selected_elements.length > 0
803 if values = ask_new_frame_offset(nil, '')
804 distribute_multiple_call.call(:frame_offset, values)
807 closures[:frame_offset].call
812 menu.append( Gtk::SeparatorMenuItem.new)
813 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
814 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
815 whitebalance.signal_connect('activate') {
816 if possible_actions[:can_multiple] && $selected_elements.length > 0
817 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
818 distribute_multiple_call.call(:whitebalance, values)
821 closures[:whitebalance].call
824 if !possible_actions[:can_multiple] || $selected_elements.length == 0
825 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
826 _("Enhance constrast"))))
828 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
830 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
831 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
832 if type == 'image' && possible_actions[:can_panorama]
833 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
834 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
835 panorama.signal_connect('activate') {
836 if possible_actions[:can_multiple] && $selected_elements.length > 0
837 if values = ask_new_pano_amount(nil, '')
838 distribute_multiple_call.call(:pano, values)
841 distribute_multiple_call.call(:pano)
845 if optionals.include?('delete')
846 menu.append( Gtk::SeparatorMenuItem.new)
847 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
848 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
849 if !possible_actions[:can_multiple] || $selected_elements.length == 0
850 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
851 paste_item.signal_connect('activate') { closures[:paste].call }
852 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
853 clear_item.signal_connect('activate') { $cuts = [] }
855 paste_item.sensitive = clear_item.sensitive = false
858 menu.append( Gtk::SeparatorMenuItem.new)
859 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
860 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
863 menu.popup(nil, nil, event.button, event.time)
866 def delete_current_subalbum
868 sel = $albums_tv.selection.selected_rows
869 $xmldir.elements.each { |e|
870 if e.name == 'image' || e.name == 'video'
871 e.add_attribute('deleted', 'true')
874 #- branch if we have a non deleted subalbum
875 if $xmldir.child_byname_notattr('dir', 'deleted')
876 $xmldir.delete_attribute('thumbnails-caption')
877 $xmldir.delete_attribute('thumbnails-captionfile')
879 $xmldir.add_attribute('deleted', 'true')
881 while moveup.parent.name == 'dir'
882 moveup = moveup.parent
883 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
884 moveup.add_attribute('deleted', 'true')
891 save_changes('forced')
892 populate_subalbums_treeview(false)
893 $albums_tv.selection.select_path(sel[0])
899 $current_path = nil #- prevent save_changes from being rerun again
900 sel = $albums_tv.selection.selected_rows
901 restore_one = proc { |xmldir|
902 xmldir.elements.each { |e|
903 if e.name == 'dir' && e.attributes['deleted']
906 e.delete_attribute('deleted')
909 restore_one.call($xmldir)
910 populate_subalbums_treeview(false)
911 $albums_tv.selection.select_path(sel[0])
914 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
917 frame1 = Gtk::Frame.new
918 fullpath = from_utf8("#{$current_path}/#{filename}")
920 my_gen_real_thumbnail = proc {
921 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
924 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
925 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
926 frame1.add(img = Gtk::Image.new)
927 my_gen_real_thumbnail.call
929 frame1.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
931 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
933 tooltips = Gtk::Tooltips.new
934 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
935 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
937 frame2, textview = create_editzone($autotable_sw, 1, img)
938 textview.buffer.text = utf8(caption)
939 textview.set_justification(Gtk::Justification::CENTER)
941 vbox = Gtk::VBox.new(false, 5)
942 vbox.pack_start(evtbox, false, false)
943 vbox.pack_start(frame2, false, false)
944 autotable.append(vbox, filename)
946 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
947 $vbox2widgets[vbox] = { :textview => textview, :image => img }
949 #- to be able to find widgets by name
950 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
952 cleanup_all_thumbnails = Proc.new {
953 #- remove out of sync images
954 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
955 for sizeobj in $images_size
956 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
961 rotate_and_cleanup = Proc.new { |angle|
962 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
963 cleanup_all_thumbnails.call
966 move = Proc.new { |direction|
967 do_method = "move_#{direction}"
968 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
970 done = autotable.method(do_method).call(vbox)
971 textview.grab_focus #- because if moving, focus is stolen
975 save_undo(_("move %s") % direction,
977 autotable.method(undo_method).call(vbox)
978 textview.grab_focus #- because if moving, focus is stolen
979 autoscroll_if_needed($autotable_sw, img, textview)
980 $notebook.set_page(1)
982 autotable.method(do_method).call(vbox)
983 textview.grab_focus #- because if moving, focus is stolen
984 autoscroll_if_needed($autotable_sw, img, textview)
985 $notebook.set_page(1)
991 color_swap_and_cleanup = Proc.new {
992 perform_color_swap_and_cleanup = Proc.new {
993 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
994 my_gen_real_thumbnail.call
997 cleanup_all_thumbnails.call
998 perform_color_swap_and_cleanup.call
1000 save_undo(_("color swap"),
1002 perform_color_swap_and_cleanup.call
1004 autoscroll_if_needed($autotable_sw, img, textview)
1005 $notebook.set_page(1)
1007 perform_color_swap_and_cleanup.call
1009 autoscroll_if_needed($autotable_sw, img, textview)
1010 $notebook.set_page(1)
1015 change_frame_offset_and_cleanup_real = Proc.new { |values|
1016 perform_change_frame_offset_and_cleanup = Proc.new { |val|
1017 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
1018 my_gen_real_thumbnail.call
1020 perform_change_frame_offset_and_cleanup.call(values[:new])
1022 save_undo(_("specify frame offset"),
1024 perform_change_frame_offset_and_cleanup.call(values[:old])
1026 autoscroll_if_needed($autotable_sw, img, textview)
1027 $notebook.set_page(1)
1029 perform_change_frame_offset_and_cleanup.call(values[:new])
1031 autoscroll_if_needed($autotable_sw, img, textview)
1032 $notebook.set_page(1)
1037 change_frame_offset_and_cleanup = Proc.new {
1038 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
1039 change_frame_offset_and_cleanup_real.call(values)
1043 change_pano_amount_and_cleanup_real = Proc.new { |values|
1044 perform_change_pano_amount_and_cleanup = Proc.new { |val|
1045 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1047 perform_change_pano_amount_and_cleanup.call(values[:new])
1049 save_undo(_("change panorama amount"),
1051 perform_change_pano_amount_and_cleanup.call(values[:old])
1053 autoscroll_if_needed($autotable_sw, img, textview)
1054 $notebook.set_page(1)
1056 perform_change_pano_amount_and_cleanup.call(values[:new])
1058 autoscroll_if_needed($autotable_sw, img, textview)
1059 $notebook.set_page(1)
1064 change_pano_amount_and_cleanup = Proc.new {
1065 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1066 change_pano_amount_and_cleanup_real.call(values)
1070 whitebalance_and_cleanup_real = Proc.new { |values|
1071 perform_change_whitebalance_and_cleanup = Proc.new { |val|
1072 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1073 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1074 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1075 cleanup_all_thumbnails.call
1077 perform_change_whitebalance_and_cleanup.call(values[:new])
1079 save_undo(_("fix white balance"),
1081 perform_change_whitebalance_and_cleanup.call(values[:old])
1083 autoscroll_if_needed($autotable_sw, img, textview)
1084 $notebook.set_page(1)
1086 perform_change_whitebalance_and_cleanup.call(values[:new])
1088 autoscroll_if_needed($autotable_sw, img, textview)
1089 $notebook.set_page(1)
1094 whitebalance_and_cleanup = Proc.new {
1095 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1096 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1097 whitebalance_and_cleanup_real.call(values)
1101 enhance_and_cleanup = Proc.new {
1102 perform_enhance_and_cleanup = Proc.new {
1103 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1104 my_gen_real_thumbnail.call
1107 cleanup_all_thumbnails.call
1108 perform_enhance_and_cleanup.call
1110 save_undo(_("enhance"),
1112 perform_enhance_and_cleanup.call
1114 autoscroll_if_needed($autotable_sw, img, textview)
1115 $notebook.set_page(1)
1117 perform_enhance_and_cleanup.call
1119 autoscroll_if_needed($autotable_sw, img, textview)
1120 $notebook.set_page(1)
1125 delete = Proc.new { |isacut|
1126 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 })
1129 perform_delete = Proc.new {
1130 after = autotable.get_next_widget(vbox)
1132 after = autotable.get_previous_widget(vbox)
1134 if $config['deleteondisk'] && !isacut
1135 msg 3, "scheduling for delete: #{fullpath}"
1136 $todelete << fullpath
1138 autotable.remove(vbox)
1140 $vbox2widgets[after][:textview].grab_focus
1141 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1145 previous_pos = autotable.get_current_number(vbox)
1149 delete_current_subalbum
1151 save_undo(_("delete"),
1153 autotable.reinsert(pos, vbox, filename)
1154 $notebook.set_page(1)
1155 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1157 msg 3, "removing deletion schedule of: #{fullpath}"
1158 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1161 $notebook.set_page(1)
1170 $cuts << { :vbox => vbox, :filename => filename }
1171 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1176 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1179 autotable.queue_draws << proc {
1180 $vbox2widgets[last[:vbox]][:textview].grab_focus
1181 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1183 save_undo(_("paste"),
1185 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1186 $notebook.set_page(1)
1189 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1191 $notebook.set_page(1)
1194 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1199 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1200 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1201 :whitebalance => whitebalance_and_cleanup_real, :pano => change_pano_amount_and_cleanup_real }
1203 textview.signal_connect('key-press-event') { |w, event|
1206 x, y = autotable.get_current_pos(vbox)
1207 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1208 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1209 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1210 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1212 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1213 $vbox2widgets[widget_up][:textview].grab_focus
1220 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1222 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1223 $vbox2widgets[widget_down][:textview].grab_focus
1230 if event.keyval == Gdk::Keyval::GDK_Left
1233 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1240 rotate_and_cleanup.call(-90)
1243 if event.keyval == Gdk::Keyval::GDK_Right
1244 next_ = autotable.get_next_widget(vbox)
1245 if next_ && autotable.get_current_pos(next_)[0] > x
1247 $vbox2widgets[next_][:textview].grab_focus
1254 rotate_and_cleanup.call(90)
1257 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1260 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1261 view_element(filename, { :delete => delete })
1264 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1267 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1271 !propagate #- propagate if needed
1274 $ignore_next_release = false
1275 evtbox.signal_connect('button-press-event') { |w, event|
1276 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1277 if event.state & Gdk::Window::BUTTON3_MASK != 0
1278 #- gesture redo: hold right mouse button then click left mouse button
1279 $config['nogestures'] or perform_redo
1280 $ignore_next_release = true
1282 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1284 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1286 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1287 elsif $enhance.active?
1288 enhance_and_cleanup.call
1289 elsif $delete.active?
1293 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1296 $button1_pressed_autotable = true
1297 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1298 if event.state & Gdk::Window::BUTTON1_MASK != 0
1299 #- gesture undo: hold left mouse button then click right mouse button
1300 $config['nogestures'] or perform_undo
1301 $ignore_next_release = true
1303 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1304 view_element(filename, { :delete => delete })
1309 evtbox.signal_connect('button-release-event') { |w, event|
1310 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1311 if !$ignore_next_release
1312 x, y = autotable.get_current_pos(vbox)
1313 next_ = autotable.get_next_widget(vbox)
1314 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1315 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1316 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1317 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1318 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1319 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1320 :pano => change_pano_amount_and_cleanup })
1322 $ignore_next_release = false
1323 $gesture_press = nil
1328 #- handle reordering with drag and drop
1329 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1330 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1331 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1332 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1335 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1337 #- mouse gesture first (dnd disables button-release-event)
1338 if $gesture_press && $gesture_press[:filename] == filename
1339 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1340 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1341 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1342 rotate_and_cleanup.call(angle)
1343 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1345 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1346 msg 3, "gesture delete: click-drag right button to the bottom"
1348 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1353 ctxt.targets.each { |target|
1354 if target.name == 'reorder-elements'
1355 move_dnd = Proc.new { |from,to|
1358 autotable.move(from, to)
1359 save_undo(_("reorder"),
1360 Proc.new { |from, to|
1362 autotable.move(to - 1, from)
1364 autotable.move(to, from + 1)
1366 $notebook.set_page(1)
1368 autotable.move(from, to)
1369 $notebook.set_page(1)
1374 if $multiple_dnd.size == 0
1375 move_dnd.call(selection_data.data.to_i,
1376 autotable.get_current_number(vbox))
1378 UndoHandler.begin_batch
1379 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1381 #- need to update current position between each call
1382 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1383 autotable.get_current_number(vbox))
1385 UndoHandler.end_batch
1396 def create_auto_table
1398 $autotable = Gtk::AutoTable.new(5)
1400 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1401 thumbnails_vb = Gtk::VBox.new(false, 5)
1403 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1404 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1405 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1406 thumbnails_vb.add($autotable)
1408 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1409 $autotable_sw.add_with_viewport(thumbnails_vb)
1411 #- follows stuff for handling multiple elements selection
1412 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1414 update_selected = Proc.new {
1415 $autotable.current_order.each { |path|
1416 w = $name2widgets[path][:evtbox].window
1417 xm = w.position[0] + w.size[0]/2
1418 ym = w.position[1] + w.size[1]/2
1419 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1420 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1421 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1422 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1425 if $selected_elements[path] && ! $selected_elements[path][:keep]
1426 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))
1427 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1428 $selected_elements.delete(path)
1433 $autotable.signal_connect('realize') { |w,e|
1434 gc = Gdk::GC.new($autotable.window)
1435 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1436 gc.function = Gdk::GC::INVERT
1437 #- autoscroll handling for DND and multiple selections
1438 Gtk.timeout_add(100) {
1439 w, x, y, mask = $autotable.window.pointer
1440 if mask & Gdk::Window::BUTTON1_MASK != 0
1441 if y < $autotable_sw.vadjustment.value
1443 $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]])
1445 if $button1_pressed_autotable || press_x
1446 scroll_upper($autotable_sw, y)
1449 w, pos_x, pos_y = $autotable.window.pointer
1450 $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]])
1451 update_selected.call
1454 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1456 $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]])
1458 if $button1_pressed_autotable || press_x
1459 scroll_lower($autotable_sw, y)
1462 w, pos_x, pos_y = $autotable.window.pointer
1463 $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]])
1464 update_selected.call
1472 $autotable.signal_connect('button-press-event') { |w,e|
1474 if !$button1_pressed_autotable
1477 if e.state & Gdk::Window::SHIFT_MASK == 0
1478 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1479 $selected_elements = {}
1480 $statusbar.push(0, utf8(_("Nothing selected.")))
1482 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1484 set_mousecursor(Gdk::Cursor::TCROSS)
1488 $autotable.signal_connect('button-release-event') { |w,e|
1490 if $button1_pressed_autotable
1491 #- unselect all only now
1492 $multiple_dnd = $selected_elements.keys
1493 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1494 $selected_elements = {}
1495 $button1_pressed_autotable = false
1498 $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]])
1499 if $selected_elements.length > 0
1500 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1503 press_x = press_y = pos_x = pos_y = nil
1504 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1508 $autotable.signal_connect('motion-notify-event') { |w,e|
1511 $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]])
1515 $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]])
1516 update_selected.call
1522 def create_subalbums_page
1524 subalbums_hb = Gtk::HBox.new
1525 $subalbums_vb = Gtk::VBox.new(false, 5)
1526 subalbums_hb.pack_start($subalbums_vb, false, false)
1527 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1528 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1529 $subalbums_sw.add_with_viewport(subalbums_hb)
1532 def save_current_file
1536 ios = File.open($filename, "w")
1537 $xmldoc.write(ios, 0)
1542 def save_current_file_user
1543 save_tempfilename = $filename
1544 $filename = $orig_filename
1547 $generated_outofline = false
1548 $filename = save_tempfilename
1550 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1551 $todelete.each { |f|
1552 system("rm -f #{f}")
1556 def mark_document_as_dirty
1557 $xmldoc.elements.each('//dir') { |elem|
1558 elem.delete_attribute('already-generated')
1562 #- ret: true => ok false => cancel
1563 def ask_save_modifications(msg1, msg2, *options)
1565 options = options.size > 0 ? options[0] : {}
1567 if options[:disallow_cancel]
1568 dialog = Gtk::Dialog.new(msg1,
1570 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1571 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1572 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1574 dialog = Gtk::Dialog.new(msg1,
1576 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1577 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1578 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1579 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1581 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1582 dialog.vbox.add(Gtk::Label.new(msg2))
1583 dialog.window_position = Gtk::Window::POS_CENTER
1586 dialog.run { |response|
1588 if response == Gtk::Dialog::RESPONSE_YES
1589 save_current_file_user
1591 #- if we have generated an album but won't save modifications, we must remove
1592 #- already-generated markers in original file
1593 if $generated_outofline
1595 $xmldoc = REXML::Document.new File.new($orig_filename)
1596 mark_document_as_dirty
1597 ios = File.open($orig_filename, "w")
1598 $xmldoc.write(ios, 0)
1601 puts "exception: #{$!}"
1605 if response == Gtk::Dialog::RESPONSE_CANCEL
1608 $todelete = [] #- unconditionally clear the list of images/videos to delete
1614 def try_quit(*options)
1615 if ask_save_modifications(utf8(_("Save before quitting?")),
1616 utf8(_("Do you want to save your changes before quitting?")),
1622 def show_popup(parent, msg, *options)
1623 dialog = Gtk::Dialog.new
1624 if options[0] && options[0][:title]
1625 dialog.title = options[0][:title]
1627 dialog.title = utf8(_("Booh message"))
1629 lbl = Gtk::Label.new
1630 if options[0] && options[0][:nomarkup]
1635 if options[0] && options[0][:centered]
1636 lbl.set_justify(Gtk::Justification::CENTER)
1638 if options[0] && options[0][:selectable]
1639 lbl.selectable = true
1641 if options[0] && options[0][:topwidget]
1642 dialog.vbox.add(options[0][:topwidget])
1644 if options[0] && options[0][:scrolled]
1645 sw = Gtk::ScrolledWindow.new(nil, nil)
1646 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1647 sw.add_with_viewport(lbl)
1649 dialog.set_default_size(400, 500)
1651 dialog.vbox.add(lbl)
1652 dialog.set_default_size(200, 120)
1654 if options[0] && options[0][:okcancel]
1655 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1657 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1659 if options[0] && options[0][:pos_centered]
1660 dialog.window_position = Gtk::Window::POS_CENTER
1662 dialog.window_position = Gtk::Window::POS_MOUSE
1665 if options[0] && options[0][:linkurl]
1666 linkbut = Gtk::Button.new('')
1667 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1668 linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1669 linkbut.relief = Gtk::RELIEF_NONE
1670 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1671 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1672 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1677 if !options[0] || !options[0][:not_transient]
1678 dialog.transient_for = parent
1679 dialog.run { |response|
1681 if options[0] && options[0][:okcancel]
1682 return response == Gtk::Dialog::RESPONSE_OK
1686 dialog.signal_connect('response') { dialog.destroy }
1690 def backend_wait_message(parent, msg, infopipe_path, mode)
1692 w.set_transient_for(parent)
1695 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1696 vb.pack_start(Gtk::Label.new(msg), false, false)
1698 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1699 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1700 if mode != 'one dir scan'
1701 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1703 if mode == 'web-album'
1704 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1705 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1707 vb.pack_start(Gtk::HSeparator.new, false, false)
1709 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1710 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1711 vb.pack_end(bottom, false, false)
1713 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1714 refresh_thread = Thread.new {
1715 directories_counter = 0
1716 while line = infopipe.gets
1717 if line =~ /^directories: (\d+), sizes: (\d+)/
1718 directories = $1.to_f + 1
1720 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1721 elements = $3.to_f + 1
1722 if mode == 'web-album'
1726 gtk_thread_protect { pb1_1.fraction = 0 }
1727 if mode != 'one dir scan'
1728 newtext = utf8(full_src_dir_to_rel($1, $2))
1729 newtext = '/' if newtext == ''
1730 gtk_thread_protect { pb1_2.text = newtext }
1731 directories_counter += 1
1732 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1734 elsif line =~ /^processing element$/
1735 element_counter += 1
1736 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1737 elsif line =~ /^processing size$/
1738 element_counter += 1
1739 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1740 elsif line =~ /^finished processing sizes$/
1741 gtk_thread_protect { pb1_1.fraction = 1 }
1742 elsif line =~ /^creating index.html$/
1743 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1744 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1745 directories_counter = 0
1746 elsif line =~ /^index.html: (.+)\|(.+)/
1747 newtext = utf8(full_src_dir_to_rel($1, $2))
1748 newtext = '/' if newtext == ''
1749 gtk_thread_protect { pb2.text = newtext }
1750 directories_counter += 1
1751 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1752 elsif line =~ /^die: (.*)$/
1759 w.signal_connect('delete-event') { w.destroy }
1760 w.signal_connect('destroy') {
1761 Thread.kill(refresh_thread)
1762 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1765 system("rm -f #{infopipe_path}")
1768 w.window_position = Gtk::Window::POS_CENTER
1774 def call_backend(cmd, waitmsg, mode, params)
1775 pipe = Tempfile.new("boohpipe")
1777 system("mkfifo #{pipe.path}")
1778 cmd += " --info-pipe #{pipe.path}"
1779 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
1784 id, exitstatus = Process.waitpid2(pid)
1785 gtk_thread_protect { w8.destroy }
1787 if params[:successmsg]
1788 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
1790 if params[:closure_after]
1791 gtk_thread_protect(¶ms[:closure_after])
1793 elsif exitstatus == 15
1794 #- say nothing, user aborted
1796 gtk_thread_protect { show_popup($main_window,
1797 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
1803 button.signal_connect('clicked') {
1804 Process.kill('SIGTERM', pid)
1808 def save_changes(*forced)
1809 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1813 $xmldir.delete_attribute('already-generated')
1815 propagate_children = Proc.new { |xmldir|
1816 if xmldir.attributes['subdirs-caption']
1817 xmldir.delete_attribute('already-generated')
1819 xmldir.elements.each('dir') { |element|
1820 propagate_children.call(element)
1824 if $xmldir.child_byname_notattr('dir', 'deleted')
1825 new_title = $subalbums_title.buffer.text
1826 if new_title != $xmldir.attributes['subdirs-caption']
1827 parent = $xmldir.parent
1828 if parent.name == 'dir'
1829 parent.delete_attribute('already-generated')
1831 propagate_children.call($xmldir)
1833 $xmldir.add_attribute('subdirs-caption', new_title)
1834 $xmldir.elements.each('dir') { |element|
1835 if !element.attributes['deleted']
1836 path = element.attributes['path']
1837 newtext = $subalbums_edits[path][:editzone].buffer.text
1838 if element.attributes['subdirs-caption']
1839 if element.attributes['subdirs-caption'] != newtext
1840 propagate_children.call(element)
1842 element.add_attribute('subdirs-caption', newtext)
1843 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1845 if element.attributes['thumbnails-caption'] != newtext
1846 element.delete_attribute('already-generated')
1848 element.add_attribute('thumbnails-caption', newtext)
1849 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1855 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
1856 if $xmldir.attributes['thumbnails-caption']
1857 path = $xmldir.attributes['path']
1858 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1860 elsif $xmldir.attributes['thumbnails-caption']
1861 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
1864 #- remove and reinsert elements to reflect new ordering
1867 $xmldir.elements.each { |element|
1868 if element.name == 'image' || element.name == 'video'
1869 saves[element.attributes['filename']] = element.remove
1873 $autotable.current_order.each { |path|
1874 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1875 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1878 saves.each_key { |path|
1879 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1880 chld.add_attribute('deleted', 'true')
1884 def sort_by_exif_date
1888 $xmldir.elements.each { |element|
1889 if element.name == 'image' || element.name == 'video'
1890 current_order << element.attributes['filename']
1894 #- look for EXIF dates
1896 w.set_transient_for($main_window)
1898 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1899 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
1900 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
1901 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1902 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1903 vb.pack_end(bottom, false, false)
1905 w.signal_connect('delete-event') { w.destroy }
1906 w.window_position = Gtk::Window::POS_CENTER
1910 b.signal_connect('clicked') { aborted = true }
1913 current_order.each { |f|
1915 if entry2type(f) == 'image'
1917 pb.fraction = i.to_f / current_order.size
1918 Gtk.main_iteration while Gtk.events_pending?
1919 date_time = `identify -format "%[EXIF:DateTime]" '#{from_utf8($current_path + "/" + f)}'`.chomp
1920 if $? == 0 && date_time != ''
1921 dates[f] = date_time
1934 $xmldir.elements.each { |element|
1935 if element.name == 'image' || element.name == 'video'
1936 saves[element.attributes['filename']] = element.remove
1940 #- find a good fallback for all entries without a date (still next to the item they were next to)
1941 neworder = dates.keys.sort { |a,b| dates[a] <=> dates[b] }
1942 for i in 0 .. current_order.size - 1
1943 if ! neworder.include?(current_order[i])
1945 while j > 0 && ! neworder.include?(current_order[j])
1948 neworder[(neworder.index(current_order[j]) || -1 ) + 1, 0] = current_order[i]
1952 $xmldir.add_element(saves[f].name, saves[f].attributes)
1955 #- let the auto-table reflect new ordering
1959 def remove_all_captions
1962 $autotable.current_order.each { |path|
1963 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
1964 $name2widgets[File.basename(path)][:textview].buffer.text = ''
1966 save_undo(_("remove all captions"),
1968 texts.each_key { |key|
1969 $name2widgets[key][:textview].buffer.text = texts[key]
1971 $notebook.set_page(1)
1973 texts.each_key { |key|
1974 $name2widgets[key][:textview].buffer.text = ''
1976 $notebook.set_page(1)
1982 $selected_elements.each_key { |path|
1983 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1989 $selected_elements = {}
1993 $undo_tb.sensitive = $undo_mb.sensitive = false
1994 $redo_tb.sensitive = $redo_mb.sensitive = false
2000 $subalbums_vb.children.each { |chld|
2001 $subalbums_vb.remove(chld)
2003 $subalbums = Gtk::Table.new(0, 0, true)
2004 current_y_sub_albums = 0
2006 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2007 $subalbums_edits = {}
2008 subalbums_counter = 0
2009 subalbums_edits_bypos = {}
2011 add_subalbum = Proc.new { |xmldir, counter|
2012 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2013 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2014 if xmldir == $xmldir
2015 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2016 caption = xmldir.attributes['thumbnails-caption']
2017 captionfile, dummy = find_subalbum_caption_info(xmldir)
2018 infotype = 'thumbnails'
2020 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2021 captionfile, caption = find_subalbum_caption_info(xmldir)
2022 infotype = find_subalbum_info_type(xmldir)
2024 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2025 hbox = Gtk::HBox.new
2026 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2028 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2031 my_gen_real_thumbnail = proc {
2032 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2035 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2036 f.add(img = Gtk::Image.new)
2037 my_gen_real_thumbnail.call
2039 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2041 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2042 $subalbums.attach(hbox,
2043 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2045 frame, textview = create_editzone($subalbums_sw, 0, img)
2046 textview.buffer.text = caption
2047 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2048 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2050 change_image = Proc.new {
2051 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2053 Gtk::FileChooser::ACTION_OPEN,
2055 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2056 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2057 fc.transient_for = $main_window
2058 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))
2059 f.add(preview_img = Gtk::Image.new)
2061 fc.signal_connect('update-preview') { |w|
2063 if fc.preview_filename
2064 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2065 fc.preview_widget_active = true
2067 rescue Gdk::PixbufError
2068 fc.preview_widget_active = false
2071 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2073 old_file = captionfile
2074 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2075 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2076 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2077 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2079 new_file = fc.filename
2080 msg 3, "new captionfile is: #{fc.filename}"
2081 perform_changefile = Proc.new {
2082 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2083 $modified_pixbufs.delete(thumbnail_file)
2084 xmldir.delete_attribute("#{infotype}-rotate")
2085 xmldir.delete_attribute("#{infotype}-color-swap")
2086 xmldir.delete_attribute("#{infotype}-enhance")
2087 xmldir.delete_attribute("#{infotype}-frame-offset")
2088 my_gen_real_thumbnail.call
2090 perform_changefile.call
2092 save_undo(_("change caption file for sub-album"),
2094 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2095 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2096 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2097 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2098 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2099 my_gen_real_thumbnail.call
2100 $notebook.set_page(0)
2102 perform_changefile.call
2103 $notebook.set_page(0)
2110 rotate_and_cleanup = Proc.new { |angle|
2111 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2112 system("rm -f '#{thumbnail_file}'")
2115 move = Proc.new { |direction|
2118 save_changes('forced')
2119 if direction == 'up'
2120 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2121 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2122 subalbums_edits_bypos[oldpos - 1][:position] += 1
2124 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2125 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2126 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2130 $xmldir.elements.each('dir') { |element|
2131 if (!element.attributes['deleted'])
2132 elems << [ element.attributes['path'], element.remove ]
2135 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2136 each { |e| $xmldir.add_element(e[1]) }
2137 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2138 $xmldir.elements.each('descendant::dir') { |elem|
2139 elem.delete_attribute('already-generated')
2144 color_swap_and_cleanup = Proc.new {
2145 perform_color_swap_and_cleanup = Proc.new {
2146 color_swap(xmldir, "#{infotype}-")
2147 my_gen_real_thumbnail.call
2149 perform_color_swap_and_cleanup.call
2151 save_undo(_("color swap"),
2153 perform_color_swap_and_cleanup.call
2154 $notebook.set_page(0)
2156 perform_color_swap_and_cleanup.call
2157 $notebook.set_page(0)
2162 change_frame_offset_and_cleanup = Proc.new {
2163 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2164 perform_change_frame_offset_and_cleanup = Proc.new { |val|
2165 change_frame_offset(xmldir, "#{infotype}-", val)
2166 my_gen_real_thumbnail.call
2168 perform_change_frame_offset_and_cleanup.call(values[:new])
2170 save_undo(_("specify frame offset"),
2172 perform_change_frame_offset_and_cleanup.call(values[:old])
2173 $notebook.set_page(0)
2175 perform_change_frame_offset_and_cleanup.call(values[:new])
2176 $notebook.set_page(0)
2182 whitebalance_and_cleanup = Proc.new {
2183 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2184 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2185 perform_change_whitebalance_and_cleanup = Proc.new { |val|
2186 change_whitebalance(xmldir, "#{infotype}-", val)
2187 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2188 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2189 system("rm -f '#{thumbnail_file}'")
2191 perform_change_whitebalance_and_cleanup.call(values[:new])
2193 save_undo(_("fix white balance"),
2195 perform_change_whitebalance_and_cleanup.call(values[:old])
2196 $notebook.set_page(0)
2198 perform_change_whitebalance_and_cleanup.call(values[:new])
2199 $notebook.set_page(0)
2205 enhance_and_cleanup = Proc.new {
2206 perform_enhance_and_cleanup = Proc.new {
2207 enhance(xmldir, "#{infotype}-")
2208 my_gen_real_thumbnail.call
2211 perform_enhance_and_cleanup.call
2213 save_undo(_("enhance"),
2215 perform_enhance_and_cleanup.call
2216 $notebook.set_page(0)
2218 perform_enhance_and_cleanup.call
2219 $notebook.set_page(0)
2224 evtbox.signal_connect('button-press-event') { |w, event|
2225 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2227 rotate_and_cleanup.call(90)
2229 rotate_and_cleanup.call(-90)
2230 elsif $enhance.active?
2231 enhance_and_cleanup.call
2234 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2235 popup_thumbnail_menu(event, ['change_image'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2236 { :forbid_left => true, :forbid_right => true,
2237 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter },
2238 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2239 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
2241 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2246 evtbox.signal_connect('button-press-event') { |w, event|
2247 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2251 evtbox.signal_connect('button-release-event') { |w, event|
2252 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2253 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2254 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2255 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2256 msg 3, "gesture rotate: #{angle}"
2257 rotate_and_cleanup.call(angle)
2260 $gesture_press = nil
2263 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2264 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2265 current_y_sub_albums += 1
2268 if $xmldir.child_byname_notattr('dir', 'deleted')
2270 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2271 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2272 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2273 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2274 #- this album image/caption
2275 if $xmldir.attributes['thumbnails-caption']
2276 add_subalbum.call($xmldir, 0)
2279 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2280 $xmldir.elements.each { |element|
2281 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2282 #- element (image or video) of this album
2283 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2284 msg 3, "dest_img: #{dest_img}"
2285 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
2286 total[element.name] += 1
2288 if element.name == 'dir' && !element.attributes['deleted']
2289 #- sub-album image/caption
2290 add_subalbum.call(element, subalbums_counter += 1)
2291 total[element.name] += 1
2294 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2295 total['image'], total['video'], total['dir'] ]))
2296 $subalbums_vb.add($subalbums)
2297 $subalbums_vb.show_all
2299 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2300 $notebook.get_tab_label($autotable_sw).sensitive = false
2301 $notebook.set_page(0)
2302 $thumbnails_title.buffer.text = ''
2304 $notebook.get_tab_label($autotable_sw).sensitive = true
2305 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2308 if !$xmldir.child_byname_notattr('dir', 'deleted')
2309 $notebook.get_tab_label($subalbums_sw).sensitive = false
2310 $notebook.set_page(1)
2312 $notebook.get_tab_label($subalbums_sw).sensitive = true
2316 def pixbuf_or_nil(filename)
2318 return Gdk::Pixbuf.new(filename)
2324 def theme_choose(current)
2325 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2327 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2328 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2329 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2331 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2332 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2333 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2334 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2335 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2336 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2337 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2338 treeview.signal_connect('button-press-event') { |w, event|
2339 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2340 dialog.response(Gtk::Dialog::RESPONSE_OK)
2344 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2346 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2349 iter[0] = File.basename(dir)
2350 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2351 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2352 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2353 if File.basename(dir) == current
2354 treeview.selection.select_iter(iter)
2358 dialog.set_default_size(700, 400)
2359 dialog.vbox.show_all
2360 dialog.run { |response|
2361 iter = treeview.selection.selected
2363 if response == Gtk::Dialog::RESPONSE_OK && iter
2364 return model.get_value(iter, 0)
2370 def show_password_protections
2371 examine_dir_elem = Proc.new { |parent_iter, xmldir, already_protected|
2372 child_iter = $albums_iters[xmldir.attributes['path']]
2373 if xmldir.attributes['password-protect']
2374 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2375 already_protected = true
2376 elsif already_protected
2377 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2379 pix = pix.saturate_and_pixelate(1, true)
2385 xmldir.elements.each('dir') { |elem|
2386 if !elem.attributes['deleted']
2387 examine_dir_elem.call(child_iter, elem, already_protected)
2391 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2394 def populate_subalbums_treeview(select_first)
2398 $subalbums_vb.children.each { |chld|
2399 $subalbums_vb.remove(chld)
2402 source = $xmldoc.root.attributes['source']
2403 msg 3, "source: #{source}"
2405 xmldir = $xmldoc.elements['//dir']
2406 if !xmldir || xmldir.attributes['path'] != source
2407 msg 1, _("Corrupted booh file...")
2411 append_dir_elem = Proc.new { |parent_iter, xmldir|
2412 child_iter = $albums_ts.append(parent_iter)
2413 child_iter[0] = File.basename(xmldir.attributes['path'])
2414 child_iter[1] = xmldir.attributes['path']
2415 $albums_iters[xmldir.attributes['path']] = child_iter
2416 msg 3, "puttin location: #{xmldir.attributes['path']}"
2417 xmldir.elements.each('dir') { |elem|
2418 if !elem.attributes['deleted']
2419 append_dir_elem.call(child_iter, elem)
2423 append_dir_elem.call(nil, xmldir)
2424 show_password_protections
2426 $albums_tv.expand_all
2428 $albums_tv.selection.select_iter($albums_ts.iter_first)
2432 def open_file(filename)
2436 $current_path = nil #- invalidate
2437 $modified_pixbufs = {}
2440 $subalbums_vb.children.each { |chld|
2441 $subalbums_vb.remove(chld)
2444 if !File.exists?(filename)
2445 return utf8(_("File not found."))
2449 $xmldoc = REXML::Document.new File.new(filename)
2454 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2455 if entry2type(filename).nil?
2456 return utf8(_("Not a booh file!"))
2458 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."))
2462 if !source = $xmldoc.root.attributes['source']
2463 return utf8(_("Corrupted booh file..."))
2466 if !dest = $xmldoc.root.attributes['destination']
2467 return utf8(_("Corrupted booh file..."))
2470 if !theme = $xmldoc.root.attributes['theme']
2471 return utf8(_("Corrupted booh file..."))
2474 if $xmldoc.root.attributes['version'] < '0.8.4'
2475 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2476 mark_document_as_dirty
2477 if $xmldoc.root.attributes['version'] < '0.8.4'
2478 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2479 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2480 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2481 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2482 if old_dest_dir != new_dest_dir
2483 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2485 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2486 xmldir.elements.each { |element|
2487 if %w(image video).include?(element.name) && !element.attributes['deleted']
2488 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2489 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2490 Dir[old_name + '*'].each { |file|
2491 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2492 file != new_file and sys("mv '#{file}' '#{new_file}'")
2495 if element.name == 'dir' && !element.attributes['deleted']
2496 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2497 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2498 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2502 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2506 $xmldoc.root.add_attribute('version', $VERSION)
2509 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2510 optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2511 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2513 $filename = filename
2514 select_theme(theme, limit_sizes, optimizefor32, nperrow)
2515 $default_size['thumbnails'] =~ /(.*)x(.*)/
2516 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2517 $albums_thumbnail_size =~ /(.*)x(.*)/
2518 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2520 populate_subalbums_treeview(true)
2522 $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
2526 def open_file_user(filename)
2527 result = open_file(filename)
2529 $config['last-opens'] ||= []
2530 if $config['last-opens'][-1] != utf8(filename)
2531 $config['last-opens'] << utf8(filename)
2533 $orig_filename = $filename
2534 tmp = Tempfile.new("boohtemp")
2537 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2539 $tempfiles << $filename << "#{$filename}.backup"
2541 $orig_filename = nil
2547 if !ask_save_modifications(utf8(_("Save this album?")),
2548 utf8(_("Do you want to save the changes to this album?")),
2549 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2552 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2554 Gtk::FileChooser::ACTION_OPEN,
2556 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2557 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2558 fc.set_current_folder(File.expand_path("~/.booh"))
2559 fc.transient_for = $main_window
2562 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2563 push_mousecursor_wait(fc)
2564 msg = open_file_user(fc.filename)
2580 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2585 def additional_booh_options
2588 options += "--mproc #{$config['mproc'].to_i} "
2590 if $config['emptycomments']
2591 options += "--empty-comments "
2597 if !ask_save_modifications(utf8(_("Save this album?")),
2598 utf8(_("Do you want to save the changes to this album?")),
2599 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2602 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2604 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2605 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2606 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2608 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2609 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2610 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2611 tbl.attach(src = Gtk::Entry.new,
2612 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2613 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2614 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2615 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2616 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2617 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2618 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2619 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2620 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2621 tbl.attach(dest = Gtk::Entry.new,
2622 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2623 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2624 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2625 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2626 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2627 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2628 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2629 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2630 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2632 tooltips = Gtk::Tooltips.new
2633 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2634 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2635 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2636 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2637 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2638 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2639 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)
2640 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2641 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2642 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2643 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2644 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)
2646 src_nb_calculated_for = ''
2648 process_src_nb = Proc.new {
2649 if src.text != src_nb_calculated_for
2650 src_nb_calculated_for = src.text
2652 Thread.kill(src_nb_thread)
2655 if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2656 if File.readable?(from_utf8(src_nb_calculated_for))
2657 src_nb_thread = Thread.new {
2658 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2659 total = { 'image' => 0, 'video' => 0, nil => 0 }
2660 `find '#{from_utf8(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2661 if File.basename(dir) =~ /^\./
2665 Dir.entries(dir.chomp).each { |file|
2666 total[entry2type(file)] += 1
2668 rescue Errno::EACCES, Errno::ENOENT
2672 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2676 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2679 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2684 timeout_src_nb = Gtk.timeout_add(100) {
2688 src_browse.signal_connect('clicked') {
2689 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2691 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2693 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2694 fc.transient_for = $main_window
2695 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2696 src.text = utf8(fc.filename)
2698 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2703 dest_browse.signal_connect('clicked') {
2704 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2706 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2708 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2709 fc.transient_for = $main_window
2710 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2711 dest.text = utf8(fc.filename)
2716 conf_browse.signal_connect('clicked') {
2717 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2719 Gtk::FileChooser::ACTION_SAVE,
2721 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2722 fc.transient_for = $main_window
2723 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2724 fc.set_current_folder(File.expand_path("~/.booh"))
2725 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2726 conf.text = utf8(fc.filename)
2733 recreate_theme_config = proc {
2734 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2736 select_theme(theme_button.label, 'all', optimize432.active?, nil)
2737 $images_size.each { |s|
2738 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2742 tooltips.set_tip(cb, utf8(s['description']), nil)
2743 theme_sizes << { :widget => cb, :value => s['name'] }
2745 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2746 tooltips = Gtk::Tooltips.new
2747 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2748 theme_sizes << { :widget => cb, :value => 'original' }
2751 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2754 $allowed_N_values.each { |n|
2756 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2758 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2760 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2764 nperrows << { :widget => rb, :value => n }
2766 nperrowradios.show_all
2768 recreate_theme_config.call
2770 theme_button.signal_connect('clicked') {
2771 if newtheme = theme_choose(theme_button.label)
2772 theme_button.label = newtheme
2773 recreate_theme_config.call
2777 dialog.vbox.add(frame1)
2778 dialog.vbox.add(frame2)
2779 dialog.window_position = Gtk::Window::POS_MOUSE
2785 dialog.run { |response|
2786 if response == Gtk::Dialog::RESPONSE_OK
2787 srcdir = from_utf8(src.text)
2788 destdir = from_utf8(dest.text)
2789 if !File.directory?(srcdir)
2790 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2792 elsif conf.text == ''
2793 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2795 elsif File.directory?(from_utf8(conf.text))
2796 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
2798 elsif destdir != make_dest_filename(destdir)
2799 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2801 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
2802 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2804 elsif File.exists?(destdir) && !File.directory?(destdir)
2805 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2807 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2808 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2810 system("mkdir '#{destdir}'")
2811 if !File.directory?(destdir)
2812 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2823 srcdir = from_utf8(src.text)
2824 destdir = from_utf8(dest.text)
2825 configskel = File.expand_path(from_utf8(conf.text))
2826 theme = theme_button.label
2827 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2828 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2829 opt432 = optimize432.active?
2830 madewith = madewithentry.text
2832 Thread.kill(src_nb_thread)
2833 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2836 Gtk.timeout_remove(timeout_src_nb)
2839 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2840 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2841 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' #{additional_booh_options}",
2842 utf8(_("Please wait while scanning source directory...")),
2844 { :closure_after => proc { open_file_user(configskel) } })
2849 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2851 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2852 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2853 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2855 source = $xmldoc.root.attributes['source']
2856 dest = $xmldoc.root.attributes['destination']
2857 theme = $xmldoc.root.attributes['theme']
2858 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2859 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2860 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2862 limit_sizes = limit_sizes.split(/,/)
2864 madewith = $xmldoc.root.attributes['made-with']
2866 tooltips = Gtk::Tooltips.new
2867 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2868 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2869 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2870 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2871 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2872 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2873 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2874 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
2875 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2876 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2877 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2878 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
2879 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2881 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2882 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2883 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2884 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2885 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2886 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2887 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)
2888 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2889 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2890 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2891 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2893 madewithentry.text = madewith
2895 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)
2899 recreate_theme_config = proc {
2900 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2902 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
2904 $images_size.each { |s|
2905 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2907 if limit_sizes.include?(s['name'])
2915 tooltips.set_tip(cb, utf8(s['description']), nil)
2916 theme_sizes << { :widget => cb, :value => s['name'] }
2918 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2919 tooltips = Gtk::Tooltips.new
2920 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2921 if limit_sizes && limit_sizes.include?('original')
2924 theme_sizes << { :widget => cb, :value => 'original' }
2927 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2930 $allowed_N_values.each { |n|
2932 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2934 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2936 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2937 nperrowradios.add(Gtk::Label.new(' '))
2938 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
2941 nperrows << { :widget => rb, :value => n.to_s }
2943 nperrowradios.show_all
2945 recreate_theme_config.call
2947 theme_button.signal_connect('clicked') {
2948 if newtheme = theme_choose(theme_button.label)
2951 theme_button.label = newtheme
2952 recreate_theme_config.call
2956 dialog.vbox.add(frame1)
2957 dialog.vbox.add(frame2)
2958 dialog.window_position = Gtk::Window::POS_MOUSE
2964 dialog.run { |response|
2965 if response == Gtk::Dialog::RESPONSE_OK
2966 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2967 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2976 save_theme = theme_button.label
2977 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
2978 save_opt432 = optimize432.active?
2979 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2980 save_madewith = madewithentry.text
2983 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith)
2984 mark_document_as_dirty
2986 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
2987 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
2988 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' #{additional_booh_options}",
2989 utf8(_("Please wait while scanning source directory...")),
2991 { :closure_after => proc {
2992 open_file($filename)
3001 sel = $albums_tv.selection.selected_rows
3003 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3004 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3005 utf8(_("Please wait while scanning source directory...")),
3007 { :closure_after => proc {
3008 open_file($filename)
3009 $albums_tv.selection.select_path(sel[0])
3017 sel = $albums_tv.selection.selected_rows
3019 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3020 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3021 utf8(_("Please wait while scanning source directory...")),
3023 { :closure_after => proc {
3024 open_file($filename)
3025 $albums_tv.selection.select_path(sel[0])
3033 theme = $xmldoc.root.attributes['theme']
3034 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3036 limit_sizes = "--sizes #{limit_sizes}"
3038 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3039 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3040 utf8(_("Please wait while scanning source directory...")),
3042 { :closure_after => proc {
3043 open_file($filename)
3049 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3051 Gtk::FileChooser::ACTION_SAVE,
3053 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3054 fc.transient_for = $main_window
3055 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3056 fc.set_current_folder(File.expand_path("~/.booh"))
3057 fc.filename = $orig_filename
3058 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3059 $orig_filename = fc.filename
3060 save_current_file_user
3061 $config['last-opens'] ||= []
3062 $config['last-opens'] << $orig_filename
3068 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3070 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3071 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3072 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3074 dialog.vbox.add(notebook = Gtk::Notebook.new)
3075 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3076 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3077 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3078 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
3079 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3080 tooltips = Gtk::Tooltips.new
3081 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3082 for example: /usr/bin/mplayer %f")), nil)
3083 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3084 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3085 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3086 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3087 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3088 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3089 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3090 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3091 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)),
3092 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3093 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)
3094 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3095 0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3096 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)
3097 tbl.attach(emptycomments_check = Gtk::CheckButton.new(utf8(_("Use empty comments for new albums"))),
3098 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3099 tooltips.set_tip(emptycomments_check, utf8(_("Normally, filenames are used as comments for new albums. Check this if you prefer empty comments.")), nil)
3100 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3101 0, 2, 5, 6, Gtk::FILL, Gtk::SHRINK, 2, 2)
3102 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)
3103 smp_check.signal_connect('toggled') {
3104 if smp_check.active?
3105 smp_hbox.sensitive = true
3107 smp_hbox.sensitive = false
3111 smp_check.active = true
3112 smp_spin.value = $config['mproc'].to_i
3114 nogestures_check.active = $config['nogestures']
3115 emptycomments_check.active = $config['emptycomments']
3116 deleteondisk_check.active = $config['deleteondisk']
3118 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3119 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3120 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3121 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3122 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3124 dialog.vbox.show_all
3125 dialog.run { |response|
3126 if response == Gtk::Dialog::RESPONSE_OK
3127 $config['video-viewer'] = video_viewer_entry.text
3128 $config['browser'] = browser_entry.text
3129 if smp_check.active?
3130 $config['mproc'] = smp_spin.value.to_i
3132 $config.delete('mproc')
3134 $config['nogestures'] = nogestures_check.active?
3135 $config['emptycomments'] = emptycomments_check.active?
3136 $config['deleteondisk'] = deleteondisk_check.active?
3138 $config['convert-enhance'] = enhance_entry.text
3145 if $undo_tb.sensitive?
3146 $redo_tb.sensitive = $redo_mb.sensitive = true
3147 if not more_undoes = UndoHandler.undo($statusbar)
3148 $undo_tb.sensitive = $undo_mb.sensitive = false
3154 if $redo_tb.sensitive?
3155 $undo_tb.sensitive = $undo_mb.sensitive = true
3156 if not more_redoes = UndoHandler.redo($statusbar)
3157 $redo_tb.sensitive = $redo_mb.sensitive = false
3162 def show_one_click_explanation(intro)
3163 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
3165 %s When such a tool is activated
3166 (<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
3167 on a thumbnail will immediately apply the desired action.
3169 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
3175 GNU GENERAL PUBLIC LICENSE
3176 Version 2, June 1991
3178 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
3179 675 Mass Ave, Cambridge, MA 02139, USA
3180 Everyone is permitted to copy and distribute verbatim copies
3181 of this license document, but changing it is not allowed.
3185 The licenses for most software are designed to take away your
3186 freedom to share and change it. By contrast, the GNU General Public
3187 License is intended to guarantee your freedom to share and change free
3188 software--to make sure the software is free for all its users. This
3189 General Public License applies to most of the Free Software
3190 Foundation's software and to any other program whose authors commit to
3191 using it. (Some other Free Software Foundation software is covered by
3192 the GNU Library General Public License instead.) You can apply it to
3195 When we speak of free software, we are referring to freedom, not
3196 price. Our General Public Licenses are designed to make sure that you
3197 have the freedom to distribute copies of free software (and charge for
3198 this service if you wish), that you receive source code or can get it
3199 if you want it, that you can change the software or use pieces of it
3200 in new free programs; and that you know you can do these things.
3202 To protect your rights, we need to make restrictions that forbid
3203 anyone to deny you these rights or to ask you to surrender the rights.
3204 These restrictions translate to certain responsibilities for you if you
3205 distribute copies of the software, or if you modify it.
3207 For example, if you distribute copies of such a program, whether
3208 gratis or for a fee, you must give the recipients all the rights that
3209 you have. You must make sure that they, too, receive or can get the
3210 source code. And you must show them these terms so they know their
3213 We protect your rights with two steps: (1) copyright the software, and
3214 (2) offer you this license which gives you legal permission to copy,
3215 distribute and/or modify the software.
3217 Also, for each author's protection and ours, we want to make certain
3218 that everyone understands that there is no warranty for this free
3219 software. If the software is modified by someone else and passed on, we
3220 want its recipients to know that what they have is not the original, so
3221 that any problems introduced by others will not reflect on the original
3222 authors' reputations.
3224 Finally, any free program is threatened constantly by software
3225 patents. We wish to avoid the danger that redistributors of a free
3226 program will individually obtain patent licenses, in effect making the
3227 program proprietary. To prevent this, we have made it clear that any
3228 patent must be licensed for everyone's free use or not licensed at all.
3230 The precise terms and conditions for copying, distribution and
3231 modification follow.
3234 GNU GENERAL PUBLIC LICENSE
3235 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
3237 0. This License applies to any program or other work which contains
3238 a notice placed by the copyright holder saying it may be distributed
3239 under the terms of this General Public License. The "Program", below,
3240 refers to any such program or work, and a "work based on the Program"
3241 means either the Program or any derivative work under copyright law:
3242 that is to say, a work containing the Program or a portion of it,
3243 either verbatim or with modifications and/or translated into another
3244 language. (Hereinafter, translation is included without limitation in
3245 the term "modification".) Each licensee is addressed as "you".
3247 Activities other than copying, distribution and modification are not
3248 covered by this License; they are outside its scope. The act of
3249 running the Program is not restricted, and the output from the Program
3250 is covered only if its contents constitute a work based on the
3251 Program (independent of having been made by running the Program).
3252 Whether that is true depends on what the Program does.
3254 1. You may copy and distribute verbatim copies of the Program's
3255 source code as you receive it, in any medium, provided that you
3256 conspicuously and appropriately publish on each copy an appropriate
3257 copyright notice and disclaimer of warranty; keep intact all the
3258 notices that refer to this License and to the absence of any warranty;
3259 and give any other recipients of the Program a copy of this License
3260 along with the Program.
3262 You may charge a fee for the physical act of transferring a copy, and
3263 you may at your option offer warranty protection in exchange for a fee.
3265 2. You may modify your copy or copies of the Program or any portion
3266 of it, thus forming a work based on the Program, and copy and
3267 distribute such modifications or work under the terms of Section 1
3268 above, provided that you also meet all of these conditions:
3270 a) You must cause the modified files to carry prominent notices
3271 stating that you changed the files and the date of any change.
3273 b) You must cause any work that you distribute or publish, that in
3274 whole or in part contains or is derived from the Program or any
3275 part thereof, to be licensed as a whole at no charge to all third
3276 parties under the terms of this License.
3278 c) If the modified program normally reads commands interactively
3279 when run, you must cause it, when started running for such
3280 interactive use in the most ordinary way, to print or display an
3281 announcement including an appropriate copyright notice and a
3282 notice that there is no warranty (or else, saying that you provide
3283 a warranty) and that users may redistribute the program under
3284 these conditions, and telling the user how to view a copy of this
3285 License. (Exception: if the Program itself is interactive but
3286 does not normally print such an announcement, your work based on
3287 the Program is not required to print an announcement.)
3290 These requirements apply to the modified work as a whole. If
3291 identifiable sections of that work are not derived from the Program,
3292 and can be reasonably considered independent and separate works in
3293 themselves, then this License, and its terms, do not apply to those
3294 sections when you distribute them as separate works. But when you
3295 distribute the same sections as part of a whole which is a work based
3296 on the Program, the distribution of the whole must be on the terms of
3297 this License, whose permissions for other licensees extend to the
3298 entire whole, and thus to each and every part regardless of who wrote it.
3300 Thus, it is not the intent of this section to claim rights or contest
3301 your rights to work written entirely by you; rather, the intent is to
3302 exercise the right to control the distribution of derivative or
3303 collective works based on the Program.
3305 In addition, mere aggregation of another work not based on the Program
3306 with the Program (or with a work based on the Program) on a volume of
3307 a storage or distribution medium does not bring the other work under
3308 the scope of this License.
3310 3. You may copy and distribute the Program (or a work based on it,
3311 under Section 2) in object code or executable form under the terms of
3312 Sections 1 and 2 above provided that you also do one of the following:
3314 a) Accompany it with the complete corresponding machine-readable
3315 source code, which must be distributed under the terms of Sections
3316 1 and 2 above on a medium customarily used for software interchange; or,
3318 b) Accompany it with a written offer, valid for at least three
3319 years, to give any third party, for a charge no more than your
3320 cost of physically performing source distribution, a complete
3321 machine-readable copy of the corresponding source code, to be
3322 distributed under the terms of Sections 1 and 2 above on a medium
3323 customarily used for software interchange; or,
3325 c) Accompany it with the information you received as to the offer
3326 to distribute corresponding source code. (This alternative is
3327 allowed only for noncommercial distribution and only if you
3328 received the program in object code or executable form with such
3329 an offer, in accord with Subsection b above.)
3331 The source code for a work means the preferred form of the work for
3332 making modifications to it. For an executable work, complete source
3333 code means all the source code for all modules it contains, plus any
3334 associated interface definition files, plus the scripts used to
3335 control compilation and installation of the executable. However, as a
3336 special exception, the source code distributed need not include
3337 anything that is normally distributed (in either source or binary
3338 form) with the major components (compiler, kernel, and so on) of the
3339 operating system on which the executable runs, unless that component
3340 itself accompanies the executable.
3342 If distribution of executable or object code is made by offering
3343 access to copy from a designated place, then offering equivalent
3344 access to copy the source code from the same place counts as
3345 distribution of the source code, even though third parties are not
3346 compelled to copy the source along with the object code.
3349 4. You may not copy, modify, sublicense, or distribute the Program
3350 except as expressly provided under this License. Any attempt
3351 otherwise to copy, modify, sublicense or distribute the Program is
3352 void, and will automatically terminate your rights under this License.
3353 However, parties who have received copies, or rights, from you under
3354 this License will not have their licenses terminated so long as such
3355 parties remain in full compliance.
3357 5. You are not required to accept this License, since you have not
3358 signed it. However, nothing else grants you permission to modify or
3359 distribute the Program or its derivative works. These actions are
3360 prohibited by law if you do not accept this License. Therefore, by
3361 modifying or distributing the Program (or any work based on the
3362 Program), you indicate your acceptance of this License to do so, and
3363 all its terms and conditions for copying, distributing or modifying
3364 the Program or works based on it.
3366 6. Each time you redistribute the Program (or any work based on the
3367 Program), the recipient automatically receives a license from the
3368 original licensor to copy, distribute or modify the Program subject to
3369 these terms and conditions. You may not impose any further
3370 restrictions on the recipients' exercise of the rights granted herein.
3371 You are not responsible for enforcing compliance by third parties to
3374 7. If, as a consequence of a court judgment or allegation of patent
3375 infringement or for any other reason (not limited to patent issues),
3376 conditions are imposed on you (whether by court order, agreement or
3377 otherwise) that contradict the conditions of this License, they do not
3378 excuse you from the conditions of this License. If you cannot
3379 distribute so as to satisfy simultaneously your obligations under this
3380 License and any other pertinent obligations, then as a consequence you
3381 may not distribute the Program at all. For example, if a patent
3382 license would not permit royalty-free redistribution of the Program by
3383 all those who receive copies directly or indirectly through you, then
3384 the only way you could satisfy both it and this License would be to
3385 refrain entirely from distribution of the Program.
3387 If any portion of this section is held invalid or unenforceable under
3388 any particular circumstance, the balance of the section is intended to
3389 apply and the section as a whole is intended to apply in other
3392 It is not the purpose of this section to induce you to infringe any
3393 patents or other property right claims or to contest validity of any
3394 such claims; this section has the sole purpose of protecting the
3395 integrity of the free software distribution system, which is
3396 implemented by public license practices. Many people have made
3397 generous contributions to the wide range of software distributed
3398 through that system in reliance on consistent application of that
3399 system; it is up to the author/donor to decide if he or she is willing
3400 to distribute software through any other system and a licensee cannot
3403 This section is intended to make thoroughly clear what is believed to
3404 be a consequence of the rest of this License.
3407 8. If the distribution and/or use of the Program is restricted in
3408 certain countries either by patents or by copyrighted interfaces, the
3409 original copyright holder who places the Program under this License
3410 may add an explicit geographical distribution limitation excluding
3411 those countries, so that distribution is permitted only in or among
3412 countries not thus excluded. In such case, this License incorporates
3413 the limitation as if written in the body of this License.
3415 9. The Free Software Foundation may publish revised and/or new versions
3416 of the General Public License from time to time. Such new versions will
3417 be similar in spirit to the present version, but may differ in detail to
3418 address new problems or concerns.
3420 Each version is given a distinguishing version number. If the Program
3421 specifies a version number of this License which applies to it and "any
3422 later version", you have the option of following the terms and conditions
3423 either of that version or of any later version published by the Free
3424 Software Foundation. If the Program does not specify a version number of
3425 this License, you may choose any version ever published by the Free Software
3428 10. If you wish to incorporate parts of the Program into other free
3429 programs whose distribution conditions are different, write to the author
3430 to ask for permission. For software which is copyrighted by the Free
3431 Software Foundation, write to the Free Software Foundation; we sometimes
3432 make exceptions for this. Our decision will be guided by the two goals
3433 of preserving the free status of all derivatives of our free software and
3434 of promoting the sharing and reuse of software generally.
3438 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
3439 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
3440 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
3441 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
3442 OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
3443 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
3444 TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
3445 PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
3446 REPAIR OR CORRECTION.
3448 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
3449 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
3450 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
3451 INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
3452 OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
3453 TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
3454 YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
3455 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
3456 POSSIBILITY OF SUCH DAMAGES.
3460 def create_menu_and_toolbar
3463 mb = Gtk::MenuBar.new
3465 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
3466 filesubmenu = Gtk::Menu.new
3467 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
3468 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
3469 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3470 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
3471 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
3472 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3473 tooltips = Gtk::Tooltips.new
3474 filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
3475 $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3476 tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
3477 filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
3478 $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3479 tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
3480 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
3481 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3482 tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums (anywhere)")), nil)
3483 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3484 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
3485 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3486 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
3487 filesubmenu.append($view_wa = Gtk::ImageMenuItem.new(utf8(_("View web-album with browser"))).set_sensitive(false))
3488 $view_wa.image = Gtk::Image.new("#{$FPATH}/images/stock-view-webalbum-16.png")
3489 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3490 filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
3491 tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
3492 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3493 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
3494 filemenu.set_submenu(filesubmenu)
3497 new.signal_connect('activate') { new_album }
3498 open.signal_connect('activate') { open_file_popup }
3499 $save.signal_connect('activate') { save_current_file_user }
3500 $save_as.signal_connect('activate') { save_as_do }
3501 $merge_current.signal_connect('activate') { merge_current }
3502 $merge_newsubs.signal_connect('activate') { merge_newsubs }
3503 $merge.signal_connect('activate') { merge }
3504 $generate.signal_connect('activate') {
3506 call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
3507 utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
3509 { :successmsg => utf8(_("Your web-album is now ready in directory '%s'.
3510 Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
3511 :successmsg_linkurl => $xmldoc.root.attributes['destination'],
3512 :closure_after => proc {
3513 $xmldoc.elements.each('//dir') { |elem|
3514 $modified ||= elem.attributes['already-generated'].nil?
3515 elem.add_attribute('already-generated', 'true')
3517 UndoHandler.cleanup #- prevent save_changes to mark current dir as not already generated
3518 $undo_tb.sensitive = $undo_mb.sensitive = false
3519 $redo_tb.sensitive = $redo_mb.sensitive = false
3521 $generated_outofline = true
3524 $view_wa.signal_connect('activate') {
3525 indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
3526 if File.exists?(indexhtml)
3529 show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
3532 $properties.signal_connect('activate') { properties }
3534 quit.signal_connect('activate') { try_quit }
3536 editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
3537 editsubmenu = Gtk::Menu.new
3538 editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
3539 editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
3540 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3541 editsubmenu.append($sort_by_exif_date = Gtk::ImageMenuItem.new(utf8(_("Sort by EXIF date"))).set_sensitive(false))
3542 $sort_by_exif_date.image = Gtk::Image.new("#{$FPATH}/images/sort_by_exif_date.png")
3543 editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
3544 $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
3545 tooltips.set_tip($remove_all_captions, utf8(_("Mainly useful when you don't want to type any caption, that will remove default captions made of filenames")), nil)
3546 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3547 editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
3548 editmenu.set_submenu(editsubmenu)
3551 $remove_all_captions.signal_connect('activate') { remove_all_captions }
3552 $sort_by_exif_date.signal_connect('activate') { sort_by_exif_date }
3554 prefs.signal_connect('activate') { preferences }
3556 helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
3557 helpsubmenu = Gtk::Menu.new
3558 helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
3559 one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
3560 helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
3561 speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
3562 helpsubmenu.append(tutos = Gtk::ImageMenuItem.new(utf8(_("Online tutorials (opens a web-browser)"))))
3563 tutos.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3564 helpsubmenu.append( Gtk::SeparatorMenuItem.new)
3565 helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
3566 helpmenu.set_submenu(helpsubmenu)
3569 one_click.signal_connect('activate') {
3570 show_one_click_explanation(_("One-Click tools are available in the toolbar."))
3573 speed.signal_connect('activate') {
3574 show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
3576 <span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
3577 <span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
3578 <span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
3579 <span foreground='darkblue'>Control-Enter</span>: for an image, open larger view; for a video, launch player
3580 <span foreground='darkblue'>Control-Delete</span>: delete image
3581 <span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
3582 <span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
3583 <span foreground='darkblue'>Control-z</span>: undo
3584 <span foreground='darkblue'>Control-r</span>: redo
3586 <span size='large' weight='bold'>Mouse gestures:</span>
3588 Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
3589 for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
3591 <span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
3592 <span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
3593 <span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
3594 <span foreground='darkblue'>Left click, hold left button, right click</span>: undo
3595 <span foreground='darkblue'>Right click, hold right button, left click</span>: redo
3596 ")), { :pos_centered => true, :not_transient => true })
3599 tutos.signal_connect('activate') {
3600 open_url('http://zarb.org/~gc/html/booh/tutorial.html')
3603 about.signal_connect('activate') {
3604 Gtk::AboutDialog.set_url_hook { |dialog, url| open_url(url) }
3605 Gtk::AboutDialog.show($main_window, { :name => 'booh',
3606 :version => $VERSION,
3607 :copyright => 'Copyright (c) 2005 Guillaume Cottenceau',
3608 :license => get_license,
3609 :website => 'http://zarb.org/~gc/html/booh.html',
3610 :authors => [ 'Guillaume Cottenceau' ],
3611 :artists => [ 'Ayo73' ],
3612 :comments => utf8(_("''The Web-Album of choice for discriminating Linux users''")),
3613 :translator_credits => utf8(_('Japanese: Masao Mutoh
3614 German: Roland Eckert
3615 French: Guillaume Cottenceau')),
3616 :logo => Gdk::Pixbuf.new("#{$FPATH}/images/logo.png") })
3621 tb = Gtk::Toolbar.new
3623 tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
3624 open.label = utf8(_("Open")) #- to avoid missing gtk2 l10n catalogs
3625 open.menu = Gtk::Menu.new
3626 open.signal_connect('clicked') { open_file_popup }
3627 open.signal_connect('show-menu') {
3628 lastopens = Gtk::Menu.new
3630 if $config['last-opens']
3631 $config['last-opens'].reverse.each { |e|
3632 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
3633 item.signal_connect('activate') {
3634 if ask_save_modifications(utf8(_("Save this album?")),
3635 utf8(_("Do you want to save the changes to this album?")),
3636 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3637 push_mousecursor_wait
3638 msg = open_file_user(from_utf8(e))
3641 show_popup($main_window, msg)
3649 open.menu = lastopens
3652 tb.insert(-1, Gtk::SeparatorToolItem.new)
3654 tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
3655 $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
3656 $r90.label = utf8(_("Rotate"))
3657 tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
3658 $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
3659 $r270.label = utf8(_("Rotate"))
3660 tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
3661 $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
3662 $enhance.label = utf8(_("Enhance"))
3663 tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
3664 $delete.label = utf8(_("Delete")) #- to avoid missing gtk2 l10n catalogs
3665 tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
3666 nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
3667 nothing.label = utf8(_("None"))
3669 tb.insert(-1, Gtk::SeparatorToolItem.new)
3671 tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
3672 tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
3675 $undo_tb.signal_connect('clicked') { perform_undo }
3676 $undo_mb.signal_connect('activate') { perform_undo }
3677 $redo_tb.signal_connect('clicked') { perform_redo }
3678 $redo_mb.signal_connect('activate') { perform_redo }
3680 one_click_explain_try = Proc.new {
3681 if !$config['one-click-explained']
3682 show_one_click_explanation(_("You have just clicked on a One-Click tool."))
3683 $config['one-click-explained'] = true
3687 $r90.signal_connect('toggled') {
3689 set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
3690 one_click_explain_try.call
3691 $r270.active = false
3692 $enhance.active = false
3693 $delete.active = false
3694 nothing.sensitive = true
3696 if !$r270.active? && !$enhance.active? && !$delete.active?
3697 set_mousecursor_normal
3698 nothing.sensitive = false
3700 nothing.sensitive = true
3704 $r270.signal_connect('toggled') {
3706 set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
3707 one_click_explain_try.call
3709 $enhance.active = false
3710 $delete.active = false
3711 nothing.sensitive = true
3713 if !$r90.active? && !$enhance.active? && !$delete.active?
3714 set_mousecursor_normal
3715 nothing.sensitive = false
3717 nothing.sensitive = true
3721 $enhance.signal_connect('toggled') {
3723 set_mousecursor(Gdk::Cursor::SPRAYCAN)
3724 one_click_explain_try.call
3726 $r270.active = false
3727 $delete.active = false
3728 nothing.sensitive = true
3730 if !$r90.active? && !$r270.active? && !$delete.active?
3731 set_mousecursor_normal
3732 nothing.sensitive = false
3734 nothing.sensitive = true
3738 $delete.signal_connect('toggled') {
3740 set_mousecursor(Gdk::Cursor::PIRATE)
3741 one_click_explain_try.call
3743 $r270.active = false
3744 $enhance.active = false
3745 nothing.sensitive = true
3747 if !$r90.active? && !$r270.active? && !$enhance.active?
3748 set_mousecursor_normal
3749 nothing.sensitive = false
3751 nothing.sensitive = true
3755 nothing.signal_connect('clicked') {
3756 $r90.active = $r270.active = $enhance.active = $delete.active = false
3757 set_mousecursor_normal
3763 def gtk_thread_protect(&proc)
3764 if Thread.current == Thread.main
3767 $protect_gtk_pending_calls.synchronize {
3768 $gtk_pending_calls << proc
3773 def gtk_thread_flush
3774 $protect_gtk_pending_calls.try_lock
3775 for closure in $gtk_pending_calls
3778 $gtk_pending_calls = []
3779 $protect_gtk_pending_calls.unlock
3782 def ask_password_protect
3783 value = $xmldir.attributes['password-protect']
3785 dialog = Gtk::Dialog.new(utf8(_("Password protect this sub-album")),
3787 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3788 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3789 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3791 lbl = Gtk::Label.new
3793 _("You can choose to <b>password protect</b> the sub-album '%s' (only available
3794 if you plan to publish your web-album with an Apache web-server). This will use
3795 the .htaccess/.htpasswd feature of Apache (not so strongly crypted password, but
3796 generally ok for protecting web contents). Users will be prompted with a dialog
3797 asking for a username and a password, failure to give the correct pair will
3799 ") % File.basename($current_path))
3800 dialog.vbox.add(lbl)
3801 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("free access")))).
3802 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("password protect with password file:")))).
3803 add(file = Gtk::Entry.new)))
3804 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0.5, 0.2).add(Gtk::HBox.new.add(bt_help = Gtk::Button.new(utf8(_("help about password file")))).
3805 add(Gtk::Label.new).
3806 add(bt_gen = Gtk::Button.new(utf8(_("generate a password file"))))))
3807 dialog.window_position = Gtk::Window::POS_MOUSE
3812 rb_yes.active = true
3816 bt_help.signal_connect('clicked') {
3817 show_popup(dialog, utf8(
3818 _("Password protection proposed here uses the .htaccess/.htpasswd features
3819 proposed by Apache. So first, be sure you will publish your web-album on an
3820 Apache web-server. Second, you will need to have a .htpasswd file accessible
3821 by Apache somewhere on the web-server disks. The password file you must
3822 provide in the dialog when choosing to password protect is the full absolute
3823 path to access this file <b>on the web-server</b> (not on your machine). Note
3824 that if you use a relative path, it will be considered relative to the
3825 Document Root of the Apache configuration.")))
3828 bt_gen.signal_connect('clicked') {
3829 gendialog = Gtk::Dialog.new(utf8(_("Generate a password file")),
3831 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3832 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3833 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3835 lbl = Gtk::Label.new
3837 _("I can generate a password file (.htpasswd for Apache) for you. Just type
3838 the username and password you wish to put in it below and validate."))
3839 gendialog.vbox.add(lbl)
3840 gendialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(Gtk::Label.new(utf8(_('Username:')))).
3841 add(user = Gtk::Entry.new).
3842 add(Gtk::Label.new(utf8(_('Password:')))).
3843 add(pass = Gtk::Entry.new)))
3844 pass.visibility = false
3845 gendialog.window_position = Gtk::Window::POS_MOUSE
3847 gendialog.run { |response|
3851 if response == Gtk::Dialog::RESPONSE_OK
3853 ary = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a + [ '.', '/' ]
3854 return ary[rand(ary.length)]
3856 fout = Tempfile.new("htpasswd")
3857 fout.write("#{u}:#{p.crypt(rand_letter + rand_letter)}\n")
3859 File.chmod(0644, fout.path)
3860 show_popup(dialog, utf8(
3861 _("The file <b>%s</b> now contains the username and the crypted password. Now
3862 copy it to a suitable location on the machine hosting the Apache web-server (better not
3863 below the Document Root), and specify this location in the password protect dialog.") % fout.path), { :selectable => true })
3868 dialog.run { |response|
3875 if response == Gtk::Dialog::RESPONSE_OK && value != newval
3877 msg 3, "changing password protection of #{$current_path} to #{newval}"
3879 $xmldir.delete_attribute('password-protect')
3881 $xmldir.add_attribute('password-protect', newval)
3883 save_undo(_("set password protection for %s") % File.basename($current_path),
3886 $xmldir.delete_attribute('password-protect')
3888 $xmldir.add_attribute('password-protect', value)
3892 $xmldir.delete_attribute('password-protect')
3894 $xmldir.add_attribute('password-protect', newval)
3898 show_password_protections
3903 def create_main_window
3905 mb, tb = create_menu_and_toolbar
3907 $albums_tv = Gtk::TreeView.new
3908 $albums_tv.set_size_request(120, -1)
3909 $albums_tv.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }))
3910 $albums_tv.append_column(tcol = Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, { :text => 0 }))
3911 $albums_tv.expander_column = tcol
3912 $albums_tv.set_headers_visible(false)
3913 $albums_tv.selection.signal_connect('changed') { |w|
3914 push_mousecursor_wait
3918 msg 3, "no selection"
3920 $current_path = $albums_ts.get_value(iter, 1)
3925 $albums_tv.signal_connect('button-release-event') { |w, event|
3926 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3 && !$current_path.nil?
3927 menu = Gtk::Menu.new
3928 menu.append(passprotect = Gtk::ImageMenuItem.new(utf8(_("Password protect"))))
3929 passprotect.image = Gtk::Image.new("#{$FPATH}/images/galeon-secure.png")
3930 passprotect.signal_connect('activate') { ask_password_protect }
3931 menu.append(restore = Gtk::ImageMenuItem.new(utf8(_("Restore deleted images/videos/subalbums"))))
3932 restore.image = Gtk::Image.new("#{$FPATH}/images/restore.png")
3933 restore.signal_connect('activate') { restore_deleted }
3934 menu.append(Gtk::SeparatorMenuItem.new)
3935 menu.append(delete = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
3936 delete.signal_connect('activate') {
3937 if show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed? This operation cannot be undone.")), { :okcancel => true })
3938 delete_current_subalbum
3942 menu.popup(nil, nil, event.button, event.time)
3946 $albums_ts = Gtk::TreeStore.new(String, String, Gdk::Pixbuf)
3947 $albums_tv.set_model($albums_ts)
3948 $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
3950 albums_sw = Gtk::ScrolledWindow.new(nil, nil)
3951 albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
3952 albums_sw.add_with_viewport($albums_tv)
3954 $notebook = Gtk::Notebook.new
3955 create_subalbums_page
3956 $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
3958 $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
3960 $notebook.signal_connect('switch-page') { |w, page, num|
3962 $delete.active = false
3963 $delete.sensitive = false
3965 $delete.sensitive = true
3967 if $xmldir && $subalbums_edits[$xmldir.attributes['path']] && textview = $subalbums_edits[$xmldir.attributes['path']][:editzone]
3969 textview.buffer.text = $thumbnails_title.buffer.text
3971 if $notebook.get_tab_label($autotable_sw).sensitive?
3972 $thumbnails_title.buffer.text = textview.buffer.text
3978 paned = Gtk::HPaned.new
3979 paned.pack1(albums_sw, false, false)
3980 paned.pack2($notebook, true, true)
3982 main_vbox = Gtk::VBox.new(false, 0)
3983 main_vbox.pack_start(mb, false, false)
3984 main_vbox.pack_start(tb, false, false)
3985 main_vbox.pack_start(paned, true, true)
3986 main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
3988 $main_window = Gtk::Window.new
3989 $main_window.add(main_vbox)
3990 $main_window.signal_connect('delete-event') {
3991 try_quit({ :disallow_cancel => true })
3994 #- read/save size and position of window
3995 if $config['pos-x'] && $config['pos-y']
3996 $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
3998 $main_window.window_position = Gtk::Window::POS_CENTER
4000 msg 3, "size: #{$config['width']}x#{$config['height']}"
4001 $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
4002 $main_window.signal_connect('configure-event') {
4003 msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
4004 x, y = $main_window.window.root_origin
4005 width, height = $main_window.window.size
4006 $config['pos-x'] = x
4007 $config['pos-y'] = y
4008 $config['width'] = width
4009 $config['height'] = height
4013 $protect_gtk_pending_calls = Mutex.new
4014 $gtk_pending_calls = []
4015 Gtk.timeout_add(100) {
4018 $protect_gtk_pending_calls.synchronize {
4019 if ! $gtk_pending_calls.empty?
4020 $gtk_pending_calls.shift.call
4022 empty = $gtk_pending_calls.empty?
4028 $statusbar.push(0, utf8(_("Ready.")))
4029 $main_window.show_all
4032 Thread.abort_on_exception = true
4042 open_file_user(ARGV[0])