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 add_thumbnail(autotable, filename, type, thumbnail_img, caption)
869 frame1 = Gtk::Frame.new
870 fullpath = from_utf8("#{$current_path}/#{filename}")
872 my_gen_real_thumbnail = proc {
873 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
876 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
877 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
878 frame1.add(img = Gtk::Image.new)
879 my_gen_real_thumbnail.call
881 frame1.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
883 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
885 tooltips = Gtk::Tooltips.new
886 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
887 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
889 frame2, textview = create_editzone($autotable_sw, 1, img)
890 textview.buffer.text = utf8(caption)
891 textview.set_justification(Gtk::Justification::CENTER)
893 vbox = Gtk::VBox.new(false, 5)
894 vbox.pack_start(evtbox, false, false)
895 vbox.pack_start(frame2, false, false)
896 autotable.append(vbox, filename)
898 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
899 $vbox2widgets[vbox] = { :textview => textview, :image => img }
901 #- to be able to find widgets by name
902 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
904 cleanup_all_thumbnails = Proc.new {
905 #- remove out of sync images
906 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
907 for sizeobj in $images_size
908 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
913 rotate_and_cleanup = Proc.new { |angle|
914 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
915 cleanup_all_thumbnails.call
918 move = Proc.new { |direction|
919 do_method = "move_#{direction}"
920 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
922 done = autotable.method(do_method).call(vbox)
923 textview.grab_focus #- because if moving, focus is stolen
927 save_undo(_("move %s") % direction,
929 autotable.method(undo_method).call(vbox)
930 textview.grab_focus #- because if moving, focus is stolen
931 autoscroll_if_needed($autotable_sw, img, textview)
932 $notebook.set_page(1)
934 autotable.method(do_method).call(vbox)
935 textview.grab_focus #- because if moving, focus is stolen
936 autoscroll_if_needed($autotable_sw, img, textview)
937 $notebook.set_page(1)
943 color_swap_and_cleanup = Proc.new {
944 perform_color_swap_and_cleanup = Proc.new {
945 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
946 my_gen_real_thumbnail.call
949 cleanup_all_thumbnails.call
950 perform_color_swap_and_cleanup.call
952 save_undo(_("color swap"),
954 perform_color_swap_and_cleanup.call
956 autoscroll_if_needed($autotable_sw, img, textview)
957 $notebook.set_page(1)
959 perform_color_swap_and_cleanup.call
961 autoscroll_if_needed($autotable_sw, img, textview)
962 $notebook.set_page(1)
967 change_frame_offset_and_cleanup_real = Proc.new { |values|
968 perform_change_frame_offset_and_cleanup = Proc.new { |val|
969 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
970 my_gen_real_thumbnail.call
972 perform_change_frame_offset_and_cleanup.call(values[:new])
974 save_undo(_("specify frame offset"),
976 perform_change_frame_offset_and_cleanup.call(values[:old])
978 autoscroll_if_needed($autotable_sw, img, textview)
979 $notebook.set_page(1)
981 perform_change_frame_offset_and_cleanup.call(values[:new])
983 autoscroll_if_needed($autotable_sw, img, textview)
984 $notebook.set_page(1)
989 change_frame_offset_and_cleanup = Proc.new {
990 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
991 change_frame_offset_and_cleanup_real.call(values)
995 change_pano_amount_and_cleanup_real = Proc.new { |values|
996 perform_change_pano_amount_and_cleanup = Proc.new { |val|
997 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
999 perform_change_pano_amount_and_cleanup.call(values[:new])
1001 save_undo(_("change panorama amount"),
1003 perform_change_pano_amount_and_cleanup.call(values[:old])
1005 autoscroll_if_needed($autotable_sw, img, textview)
1006 $notebook.set_page(1)
1008 perform_change_pano_amount_and_cleanup.call(values[:new])
1010 autoscroll_if_needed($autotable_sw, img, textview)
1011 $notebook.set_page(1)
1016 change_pano_amount_and_cleanup = Proc.new {
1017 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1018 change_pano_amount_and_cleanup_real.call(values)
1022 whitebalance_and_cleanup_real = Proc.new { |values|
1023 perform_change_whitebalance_and_cleanup = Proc.new { |val|
1024 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1025 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1026 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1027 cleanup_all_thumbnails.call
1029 perform_change_whitebalance_and_cleanup.call(values[:new])
1031 save_undo(_("fix white balance"),
1033 perform_change_whitebalance_and_cleanup.call(values[:old])
1035 autoscroll_if_needed($autotable_sw, img, textview)
1036 $notebook.set_page(1)
1038 perform_change_whitebalance_and_cleanup.call(values[:new])
1040 autoscroll_if_needed($autotable_sw, img, textview)
1041 $notebook.set_page(1)
1046 whitebalance_and_cleanup = Proc.new {
1047 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1048 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1049 whitebalance_and_cleanup_real.call(values)
1053 enhance_and_cleanup = Proc.new {
1054 perform_enhance_and_cleanup = Proc.new {
1055 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1056 my_gen_real_thumbnail.call
1059 cleanup_all_thumbnails.call
1060 perform_enhance_and_cleanup.call
1062 save_undo(_("enhance"),
1064 perform_enhance_and_cleanup.call
1066 autoscroll_if_needed($autotable_sw, img, textview)
1067 $notebook.set_page(1)
1069 perform_enhance_and_cleanup.call
1071 autoscroll_if_needed($autotable_sw, img, textview)
1072 $notebook.set_page(1)
1077 delete = Proc.new { |isacut|
1078 if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed?")), { :okcancel => true })
1081 perform_delete = Proc.new {
1082 after = autotable.get_next_widget(vbox)
1084 after = autotable.get_previous_widget(vbox)
1086 if $config['deleteondisk'] && !isacut
1087 msg 3, "scheduling for delete: #{fullpath}"
1088 $todelete << fullpath
1090 autotable.remove(vbox)
1092 $vbox2widgets[after][:textview].grab_focus
1093 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1097 previous_pos = autotable.get_current_number(vbox)
1101 if $xmldir.child_byname_notattr('dir', 'deleted')
1102 $xmldir.delete_attribute('thumbnails-caption')
1103 $xmldir.delete_attribute('thumbnails-captionfile')
1105 $xmldir.add_attribute('deleted', 'true')
1107 while moveup.parent.name == 'dir'
1108 moveup = moveup.parent
1109 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1110 moveup.add_attribute('deleted', 'true')
1116 save_changes('forced')
1117 populate_subalbums_treeview
1119 save_undo(_("delete"),
1121 autotable.reinsert(pos, vbox, filename)
1122 $notebook.set_page(1)
1123 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1125 msg 3, "removing deletion schedule of: #{fullpath}"
1126 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1129 $notebook.set_page(1)
1138 $cuts << { :vbox => vbox, :filename => filename }
1139 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1144 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1147 autotable.queue_draws << proc {
1148 $vbox2widgets[last[:vbox]][:textview].grab_focus
1149 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1151 save_undo(_("paste"),
1153 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1154 $notebook.set_page(1)
1157 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1159 $notebook.set_page(1)
1162 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1167 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1168 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1169 :whitebalance => whitebalance_and_cleanup_real, :pano => change_pano_amount_and_cleanup_real }
1171 textview.signal_connect('key-press-event') { |w, event|
1174 x, y = autotable.get_current_pos(vbox)
1175 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1176 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1177 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1178 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1180 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1181 $vbox2widgets[widget_up][:textview].grab_focus
1188 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1190 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1191 $vbox2widgets[widget_down][:textview].grab_focus
1198 if event.keyval == Gdk::Keyval::GDK_Left
1201 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1208 rotate_and_cleanup.call(-90)
1211 if event.keyval == Gdk::Keyval::GDK_Right
1212 next_ = autotable.get_next_widget(vbox)
1213 if next_ && autotable.get_current_pos(next_)[0] > x
1215 $vbox2widgets[next_][:textview].grab_focus
1222 rotate_and_cleanup.call(90)
1225 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1228 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1229 view_element(filename, { :delete => delete })
1232 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1235 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1239 !propagate #- propagate if needed
1242 $ignore_next_release = false
1243 evtbox.signal_connect('button-press-event') { |w, event|
1244 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1245 if event.state & Gdk::Window::BUTTON3_MASK != 0
1246 #- gesture redo: hold right mouse button then click left mouse button
1247 $config['nogestures'] or perform_redo
1248 $ignore_next_release = true
1250 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1252 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1254 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1255 elsif $enhance.active?
1256 enhance_and_cleanup.call
1257 elsif $delete.active?
1261 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1264 $button1_pressed_autotable = true
1265 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1266 if event.state & Gdk::Window::BUTTON1_MASK != 0
1267 #- gesture undo: hold left mouse button then click right mouse button
1268 $config['nogestures'] or perform_undo
1269 $ignore_next_release = true
1271 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1272 view_element(filename, { :delete => delete })
1277 evtbox.signal_connect('button-release-event') { |w, event|
1278 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1279 if !$ignore_next_release
1280 x, y = autotable.get_current_pos(vbox)
1281 next_ = autotable.get_next_widget(vbox)
1282 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1283 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1284 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1285 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1286 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1287 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1288 :pano => change_pano_amount_and_cleanup })
1290 $ignore_next_release = false
1291 $gesture_press = nil
1296 #- handle reordering with drag and drop
1297 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1298 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1299 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1300 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1303 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1305 #- mouse gesture first (dnd disables button-release-event)
1306 if $gesture_press && $gesture_press[:filename] == filename
1307 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1308 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1309 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1310 rotate_and_cleanup.call(angle)
1311 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1313 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1314 msg 3, "gesture delete: click-drag right button to the bottom"
1316 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1321 ctxt.targets.each { |target|
1322 if target.name == 'reorder-elements'
1323 move_dnd = Proc.new { |from,to|
1326 autotable.move(from, to)
1327 save_undo(_("reorder"),
1328 Proc.new { |from, to|
1330 autotable.move(to - 1, from)
1332 autotable.move(to, from + 1)
1334 $notebook.set_page(1)
1336 autotable.move(from, to)
1337 $notebook.set_page(1)
1342 if $multiple_dnd.size == 0
1343 move_dnd.call(selection_data.data.to_i,
1344 autotable.get_current_number(vbox))
1346 UndoHandler.begin_batch
1347 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1349 #- need to update current position between each call
1350 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1351 autotable.get_current_number(vbox))
1353 UndoHandler.end_batch
1364 def create_auto_table
1366 $autotable = Gtk::AutoTable.new(5)
1368 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1369 thumbnails_vb = Gtk::VBox.new(false, 5)
1371 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1372 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1373 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1374 thumbnails_vb.add($autotable)
1376 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1377 $autotable_sw.add_with_viewport(thumbnails_vb)
1379 #- follows stuff for handling multiple elements selection
1380 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1382 update_selected = Proc.new {
1383 $autotable.current_order.each { |path|
1384 w = $name2widgets[path][:evtbox].window
1385 xm = w.position[0] + w.size[0]/2
1386 ym = w.position[1] + w.size[1]/2
1387 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1388 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1389 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1390 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1393 if $selected_elements[path] && ! $selected_elements[path][:keep]
1394 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))
1395 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1396 $selected_elements.delete(path)
1401 $autotable.signal_connect('realize') { |w,e|
1402 gc = Gdk::GC.new($autotable.window)
1403 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1404 gc.function = Gdk::GC::INVERT
1405 #- autoscroll handling for DND and multiple selections
1406 Gtk.timeout_add(100) {
1407 w, x, y, mask = $autotable.window.pointer
1408 if mask & Gdk::Window::BUTTON1_MASK != 0
1409 if y < $autotable_sw.vadjustment.value
1411 $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]])
1413 if $button1_pressed_autotable || press_x
1414 scroll_upper($autotable_sw, y)
1417 w, pos_x, pos_y = $autotable.window.pointer
1418 $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]])
1419 update_selected.call
1422 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1424 $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]])
1426 if $button1_pressed_autotable || press_x
1427 scroll_lower($autotable_sw, y)
1430 w, pos_x, pos_y = $autotable.window.pointer
1431 $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]])
1432 update_selected.call
1440 $autotable.signal_connect('button-press-event') { |w,e|
1442 if !$button1_pressed_autotable
1445 if e.state & Gdk::Window::SHIFT_MASK == 0
1446 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1447 $selected_elements = {}
1448 $statusbar.push(0, utf8(_("Nothing selected.")))
1450 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1452 set_mousecursor(Gdk::Cursor::TCROSS)
1456 $autotable.signal_connect('button-release-event') { |w,e|
1458 if $button1_pressed_autotable
1459 #- unselect all only now
1460 $multiple_dnd = $selected_elements.keys
1461 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1462 $selected_elements = {}
1463 $button1_pressed_autotable = false
1466 $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]])
1467 if $selected_elements.length > 0
1468 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1471 press_x = press_y = pos_x = pos_y = nil
1472 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1476 $autotable.signal_connect('motion-notify-event') { |w,e|
1479 $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]])
1483 $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]])
1484 update_selected.call
1490 def create_subalbums_page
1492 subalbums_hb = Gtk::HBox.new
1493 $subalbums_vb = Gtk::VBox.new(false, 5)
1494 subalbums_hb.pack_start($subalbums_vb, false, false)
1495 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1496 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1497 $subalbums_sw.add_with_viewport(subalbums_hb)
1500 def save_current_file
1504 ios = File.open($filename, "w")
1505 $xmldoc.write(ios, 0)
1510 def save_current_file_user
1511 save_tempfilename = $filename
1512 $filename = $orig_filename
1515 $generated_outofline = false
1516 $filename = save_tempfilename
1518 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1519 $todelete.each { |f|
1520 system("rm -f #{f}")
1524 def mark_document_as_dirty
1525 $xmldoc.elements.each('//dir') { |elem|
1526 elem.delete_attribute('already-generated')
1530 #- ret: true => ok false => cancel
1531 def ask_save_modifications(msg1, msg2, *options)
1533 options = options.size > 0 ? options[0] : {}
1535 if options[:disallow_cancel]
1536 dialog = Gtk::Dialog.new(msg1,
1538 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1539 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1540 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1542 dialog = Gtk::Dialog.new(msg1,
1544 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1545 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1546 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1547 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1549 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1550 dialog.vbox.add(Gtk::Label.new(msg2))
1551 dialog.window_position = Gtk::Window::POS_CENTER
1554 dialog.run { |response|
1556 if response == Gtk::Dialog::RESPONSE_YES
1557 save_current_file_user
1559 #- if we have generated an album but won't save modifications, we must remove
1560 #- already-generated markers in original file
1561 if $generated_outofline
1563 $xmldoc = REXML::Document.new File.new($orig_filename)
1564 mark_document_as_dirty
1565 ios = File.open($orig_filename, "w")
1566 $xmldoc.write(ios, 0)
1569 puts "exception: #{$!}"
1573 if response == Gtk::Dialog::RESPONSE_CANCEL
1576 $todelete = [] #- unconditionally clear the list of images/videos to delete
1582 def try_quit(*options)
1583 if ask_save_modifications(utf8(_("Save before quitting?")),
1584 utf8(_("Do you want to save your changes before quitting?")),
1590 def show_popup(parent, msg, *options)
1591 dialog = Gtk::Dialog.new
1592 if options[0] && options[0][:title]
1593 dialog.title = options[0][:title]
1595 dialog.title = utf8(_("Booh message"))
1597 lbl = Gtk::Label.new
1598 if options[0] && options[0][:nomarkup]
1603 if options[0] && options[0][:centered]
1604 lbl.set_justify(Gtk::Justification::CENTER)
1606 if options[0] && options[0][:selectable]
1607 lbl.selectable = true
1609 if options[0] && options[0][:topwidget]
1610 dialog.vbox.add(options[0][:topwidget])
1612 if options[0] && options[0][:scrolled]
1613 sw = Gtk::ScrolledWindow.new(nil, nil)
1614 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1615 sw.add_with_viewport(lbl)
1617 dialog.set_default_size(400, 500)
1619 dialog.vbox.add(lbl)
1620 dialog.set_default_size(200, 120)
1622 if options[0] && options[0][:okcancel]
1623 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1625 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1627 if options[0] && options[0][:pos_centered]
1628 dialog.window_position = Gtk::Window::POS_CENTER
1630 dialog.window_position = Gtk::Window::POS_MOUSE
1633 if options[0] && options[0][:linkurl]
1634 linkbut = Gtk::Button.new('')
1635 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1636 linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1637 linkbut.relief = Gtk::RELIEF_NONE
1638 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1639 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1640 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1645 if !options[0] || !options[0][:not_transient]
1646 dialog.transient_for = parent
1647 dialog.run { |response|
1649 if options[0] && options[0][:okcancel]
1650 return response == Gtk::Dialog::RESPONSE_OK
1654 dialog.signal_connect('response') { dialog.destroy }
1658 def backend_wait_message(parent, msg, infopipe_path, mode)
1660 w.set_transient_for(parent)
1663 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1664 vb.pack_start(Gtk::Label.new(msg), false, false)
1666 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1667 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1668 if mode != 'one dir scan'
1669 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1671 if mode == 'web-album'
1672 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1673 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1675 vb.pack_start(Gtk::HSeparator.new, false, false)
1677 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1678 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1679 vb.pack_end(bottom, false, false)
1681 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1682 refresh_thread = Thread.new {
1683 directories_counter = 0
1684 while line = infopipe.gets
1685 if line =~ /^directories: (\d+), sizes: (\d+)/
1686 directories = $1.to_f + 1
1688 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1689 elements = $3.to_f + 1
1690 if mode == 'web-album'
1694 gtk_thread_protect { pb1_1.fraction = 0 }
1695 if mode != 'one dir scan'
1696 newtext = utf8(full_src_dir_to_rel($1, $2))
1697 newtext = '/' if newtext == ''
1698 gtk_thread_protect { pb1_2.text = newtext }
1699 directories_counter += 1
1700 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1702 elsif line =~ /^processing element$/
1703 element_counter += 1
1704 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1705 elsif line =~ /^processing size$/
1706 element_counter += 1
1707 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1708 elsif line =~ /^finished processing sizes$/
1709 gtk_thread_protect { pb1_1.fraction = 1 }
1710 elsif line =~ /^creating index.html$/
1711 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1712 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1713 directories_counter = 0
1714 elsif line =~ /^index.html: (.+)\|(.+)/
1715 newtext = utf8(full_src_dir_to_rel($1, $2))
1716 newtext = '/' if newtext == ''
1717 gtk_thread_protect { pb2.text = newtext }
1718 directories_counter += 1
1719 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1720 elsif line =~ /^die: (.*)$/
1727 w.signal_connect('delete-event') { w.destroy }
1728 w.signal_connect('destroy') {
1729 Thread.kill(refresh_thread)
1730 gtk_thread_abandon #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1733 system("rm -f #{infopipe_path}")
1736 w.window_position = Gtk::Window::POS_CENTER
1742 def call_backend(cmd, waitmsg, mode, params)
1743 pipe = Tempfile.new("boohpipe")
1745 system("mkfifo #{pipe.path}")
1746 cmd += " --info-pipe #{pipe.path}"
1747 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
1752 id, exitstatus = Process.waitpid2(pid)
1753 gtk_thread_protect { w8.destroy }
1755 if params[:successmsg]
1756 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
1758 if params[:closure_after]
1759 gtk_thread_protect(¶ms[:closure_after])
1761 elsif exitstatus == 15
1762 #- say nothing, user aborted
1764 gtk_thread_protect { show_popup($main_window,
1765 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
1771 button.signal_connect('clicked') {
1772 Process.kill('SIGTERM', pid)
1776 def save_changes(*forced)
1777 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1781 $xmldir.delete_attribute('already-generated')
1783 propagate_children = Proc.new { |xmldir|
1784 if xmldir.attributes['subdirs-caption']
1785 xmldir.delete_attribute('already-generated')
1787 xmldir.elements.each('dir') { |element|
1788 propagate_children.call(element)
1792 if $xmldir.child_byname_notattr('dir', 'deleted')
1793 new_title = $subalbums_title.buffer.text
1794 if new_title != $xmldir.attributes['subdirs-caption']
1795 parent = $xmldir.parent
1796 if parent.name == 'dir'
1797 parent.delete_attribute('already-generated')
1799 propagate_children.call($xmldir)
1801 $xmldir.add_attribute('subdirs-caption', new_title)
1802 $xmldir.elements.each('dir') { |element|
1803 if !element.attributes['deleted']
1804 path = element.attributes['path']
1805 newtext = $subalbums_edits[path][:editzone].buffer.text
1806 if element.attributes['subdirs-caption']
1807 if element.attributes['subdirs-caption'] != newtext
1808 propagate_children.call(element)
1810 element.add_attribute('subdirs-caption', newtext)
1811 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1813 if element.attributes['thumbnails-caption'] != newtext
1814 element.delete_attribute('already-generated')
1816 element.add_attribute('thumbnails-caption', newtext)
1817 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1823 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
1824 if $xmldir.attributes['thumbnails-caption']
1825 path = $xmldir.attributes['path']
1826 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1828 elsif $xmldir.attributes['thumbnails-caption']
1829 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
1832 #- remove and reinsert elements to reflect new ordering
1835 $xmldir.elements.each { |element|
1836 if element.name == 'image' || element.name == 'video'
1837 saves[element.attributes['filename']] = element.remove
1841 $autotable.current_order.each { |path|
1842 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1843 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1846 saves.each_key { |path|
1847 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1848 chld.add_attribute('deleted', 'true')
1852 def remove_all_captions
1855 $autotable.current_order.each { |path|
1856 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
1857 $name2widgets[File.basename(path)][:textview].buffer.text = ''
1859 save_undo(_("remove all captions"),
1861 texts.each_key { |key|
1862 $name2widgets[key][:textview].buffer.text = texts[key]
1864 $notebook.set_page(1)
1866 texts.each_key { |key|
1867 $name2widgets[key][:textview].buffer.text = ''
1869 $notebook.set_page(1)
1875 $selected_elements.each_key { |path|
1876 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1882 $selected_elements = {}
1886 $undo_tb.sensitive = $undo_mb.sensitive = false
1887 $redo_tb.sensitive = $redo_mb.sensitive = false
1893 $subalbums_vb.children.each { |chld|
1894 $subalbums_vb.remove(chld)
1896 $subalbums = Gtk::Table.new(0, 0, true)
1897 current_y_sub_albums = 0
1899 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
1900 $subalbums_edits = {}
1901 subalbums_counter = 0
1902 subalbums_edits_bypos = {}
1904 add_subalbum = Proc.new { |xmldir, counter|
1905 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
1906 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
1907 if xmldir == $xmldir
1908 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
1909 caption = xmldir.attributes['thumbnails-caption']
1910 captionfile, dummy = find_subalbum_caption_info(xmldir)
1911 infotype = 'thumbnails'
1913 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
1914 captionfile, caption = find_subalbum_caption_info(xmldir)
1915 infotype = find_subalbum_info_type(xmldir)
1917 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
1918 hbox = Gtk::HBox.new
1919 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
1921 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
1924 my_gen_real_thumbnail = proc {
1925 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
1928 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
1929 f.add(img = Gtk::Image.new)
1930 my_gen_real_thumbnail.call
1932 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
1934 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
1935 $subalbums.attach(hbox,
1936 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1938 frame, textview = create_editzone($subalbums_sw, 0, img)
1939 textview.buffer.text = caption
1940 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
1941 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1943 change_image = Proc.new {
1944 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
1946 Gtk::FileChooser::ACTION_OPEN,
1948 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1949 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
1950 fc.transient_for = $main_window
1951 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))
1952 f.add(preview_img = Gtk::Image.new)
1954 fc.signal_connect('update-preview') { |w|
1956 if fc.preview_filename
1957 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
1958 fc.preview_widget_active = true
1960 rescue Gdk::PixbufError
1961 fc.preview_widget_active = false
1964 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1966 old_file = captionfile
1967 old_rotate = xmldir.attributes["#{infotype}-rotate"]
1968 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
1969 old_enhance = xmldir.attributes["#{infotype}-enhance"]
1970 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
1972 new_file = fc.filename
1973 msg 3, "new captionfile is: #{fc.filename}"
1974 perform_changefile = Proc.new {
1975 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
1976 $modified_pixbufs.delete(thumbnail_file)
1977 xmldir.delete_attribute("#{infotype}-rotate")
1978 xmldir.delete_attribute("#{infotype}-color-swap")
1979 xmldir.delete_attribute("#{infotype}-enhance")
1980 xmldir.delete_attribute("#{infotype}-frame-offset")
1981 my_gen_real_thumbnail.call
1983 perform_changefile.call
1985 save_undo(_("change caption file for sub-album"),
1987 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
1988 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
1989 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
1990 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
1991 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
1992 my_gen_real_thumbnail.call
1993 $notebook.set_page(0)
1995 perform_changefile.call
1996 $notebook.set_page(0)
2003 rotate_and_cleanup = Proc.new { |angle|
2004 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2005 system("rm -f '#{thumbnail_file}'")
2008 move = Proc.new { |direction|
2011 save_changes('forced')
2012 if direction == 'up'
2013 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2014 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2015 subalbums_edits_bypos[oldpos - 1][:position] += 1
2017 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2018 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2019 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2023 $xmldir.elements.each('dir') { |element|
2024 if (!element.attributes['deleted'])
2025 elems << [ element.attributes['path'], element.remove ]
2028 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2029 each { |e| $xmldir.add_element(e[1]) }
2030 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2031 $xmldir.elements.each('descendant::dir') { |elem|
2032 elem.delete_attribute('already-generated')
2037 color_swap_and_cleanup = Proc.new {
2038 perform_color_swap_and_cleanup = Proc.new {
2039 color_swap(xmldir, "#{infotype}-")
2040 my_gen_real_thumbnail.call
2042 perform_color_swap_and_cleanup.call
2044 save_undo(_("color swap"),
2046 perform_color_swap_and_cleanup.call
2047 $notebook.set_page(0)
2049 perform_color_swap_and_cleanup.call
2050 $notebook.set_page(0)
2055 change_frame_offset_and_cleanup = Proc.new {
2056 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2057 perform_change_frame_offset_and_cleanup = Proc.new { |val|
2058 change_frame_offset(xmldir, "#{infotype}-", val)
2059 my_gen_real_thumbnail.call
2061 perform_change_frame_offset_and_cleanup.call(values[:new])
2063 save_undo(_("specify frame offset"),
2065 perform_change_frame_offset_and_cleanup.call(values[:old])
2066 $notebook.set_page(0)
2068 perform_change_frame_offset_and_cleanup.call(values[:new])
2069 $notebook.set_page(0)
2075 whitebalance_and_cleanup = Proc.new {
2076 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2077 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2078 perform_change_whitebalance_and_cleanup = Proc.new { |val|
2079 change_whitebalance(xmldir, "#{infotype}-", val)
2080 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2081 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2082 system("rm -f '#{thumbnail_file}'")
2084 perform_change_whitebalance_and_cleanup.call(values[:new])
2086 save_undo(_("fix white balance"),
2088 perform_change_whitebalance_and_cleanup.call(values[:old])
2089 $notebook.set_page(0)
2091 perform_change_whitebalance_and_cleanup.call(values[:new])
2092 $notebook.set_page(0)
2098 enhance_and_cleanup = Proc.new {
2099 perform_enhance_and_cleanup = Proc.new {
2100 enhance(xmldir, "#{infotype}-")
2101 my_gen_real_thumbnail.call
2104 perform_enhance_and_cleanup.call
2106 save_undo(_("enhance"),
2108 perform_enhance_and_cleanup.call
2109 $notebook.set_page(0)
2111 perform_enhance_and_cleanup.call
2112 $notebook.set_page(0)
2117 evtbox.signal_connect('button-press-event') { |w, event|
2118 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2120 rotate_and_cleanup.call(90)
2122 rotate_and_cleanup.call(-90)
2123 elsif $enhance.active?
2124 enhance_and_cleanup.call
2127 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2128 popup_thumbnail_menu(event, ['change_image'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2129 { :forbid_left => true, :forbid_right => true,
2130 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter },
2131 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2132 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
2134 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2139 evtbox.signal_connect('button-press-event') { |w, event|
2140 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2144 evtbox.signal_connect('button-release-event') { |w, event|
2145 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2146 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2147 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2148 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2149 msg 3, "gesture rotate: #{angle}"
2150 rotate_and_cleanup.call(angle)
2153 $gesture_press = nil
2156 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2157 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2158 current_y_sub_albums += 1
2161 if $xmldir.child_byname_notattr('dir', 'deleted')
2163 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2164 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2165 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2166 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2167 #- this album image/caption
2168 if $xmldir.attributes['thumbnails-caption']
2169 add_subalbum.call($xmldir, 0)
2172 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2173 $xmldir.elements.each { |element|
2174 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2175 #- element (image or video) of this album
2176 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2177 msg 3, "dest_img: #{dest_img}"
2178 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
2179 total[element.name] += 1
2181 if element.name == 'dir' && !element.attributes['deleted']
2182 #- sub-album image/caption
2183 add_subalbum.call(element, subalbums_counter += 1)
2184 total[element.name] += 1
2187 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2188 total['image'], total['video'], total['dir'] ]))
2189 $subalbums_vb.add($subalbums)
2190 $subalbums_vb.show_all
2192 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2193 $notebook.get_tab_label($autotable_sw).sensitive = false
2194 $notebook.set_page(0)
2195 $thumbnails_title.buffer.text = ''
2197 $notebook.get_tab_label($autotable_sw).sensitive = true
2198 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2201 if !$xmldir.child_byname_notattr('dir', 'deleted')
2202 $notebook.get_tab_label($subalbums_sw).sensitive = false
2203 $notebook.set_page(1)
2205 $notebook.get_tab_label($subalbums_sw).sensitive = true
2209 def pixbuf_or_nil(filename)
2211 return Gdk::Pixbuf.new(filename)
2217 def theme_choose(current)
2218 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2220 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2221 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2222 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2224 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2225 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2226 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2227 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2228 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2229 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2230 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2231 treeview.signal_connect('button-press-event') { |w, event|
2232 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2233 dialog.response(Gtk::Dialog::RESPONSE_OK)
2237 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2239 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2242 iter[0] = File.basename(dir)
2243 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2244 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2245 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2246 if File.basename(dir) == current
2247 treeview.selection.select_iter(iter)
2251 dialog.set_default_size(700, 400)
2252 dialog.vbox.show_all
2253 dialog.run { |response|
2254 iter = treeview.selection.selected
2256 if response == Gtk::Dialog::RESPONSE_OK && iter
2257 return model.get_value(iter, 0)
2263 def show_password_protections
2264 examine_dir_elem = Proc.new { |parent_iter, xmldir, already_protected|
2265 child_iter = $albums_iters[xmldir.attributes['path']]
2266 if xmldir.attributes['password-protect']
2267 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2268 already_protected = true
2269 elsif already_protected
2270 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2272 pix = pix.saturate_and_pixelate(1, true)
2278 xmldir.elements.each('dir') { |elem|
2279 if !elem.attributes['deleted']
2280 examine_dir_elem.call(child_iter, elem, already_protected)
2284 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2287 def populate_subalbums_treeview
2291 $subalbums_vb.children.each { |chld|
2292 $subalbums_vb.remove(chld)
2295 source = $xmldoc.root.attributes['source']
2296 msg 3, "source: #{source}"
2298 xmldir = $xmldoc.elements['//dir']
2299 if !xmldir || xmldir.attributes['path'] != source
2300 msg 1, _("Corrupted booh file...")
2304 append_dir_elem = Proc.new { |parent_iter, xmldir|
2305 child_iter = $albums_ts.append(parent_iter)
2306 child_iter[0] = File.basename(xmldir.attributes['path'])
2307 child_iter[1] = xmldir.attributes['path']
2308 $albums_iters[xmldir.attributes['path']] = child_iter
2309 msg 3, "puttin location: #{xmldir.attributes['path']}"
2310 xmldir.elements.each('dir') { |elem|
2311 if !elem.attributes['deleted']
2312 append_dir_elem.call(child_iter, elem)
2316 append_dir_elem.call(nil, xmldir)
2317 show_password_protections
2319 $albums_tv.expand_all
2320 $albums_tv.selection.select_iter($albums_ts.iter_first)
2323 def open_file(filename)
2327 $current_path = nil #- invalidate
2328 $modified_pixbufs = {}
2331 $subalbums_vb.children.each { |chld|
2332 $subalbums_vb.remove(chld)
2335 if !File.exists?(filename)
2336 return utf8(_("File not found."))
2340 $xmldoc = REXML::Document.new File.new(filename)
2345 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2346 if entry2type(filename).nil?
2347 return utf8(_("Not a booh file!"))
2349 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."))
2353 if !source = $xmldoc.root.attributes['source']
2354 return utf8(_("Corrupted booh file..."))
2357 if !dest = $xmldoc.root.attributes['destination']
2358 return utf8(_("Corrupted booh file..."))
2361 if !theme = $xmldoc.root.attributes['theme']
2362 return utf8(_("Corrupted booh file..."))
2365 if $xmldoc.root.attributes['version'] != $VERSION
2366 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2367 mark_document_as_dirty
2368 if $xmldoc.root.attributes['version'] < '0.8.4'
2369 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2370 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2371 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2372 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2373 if old_dest_dir != new_dest_dir
2374 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2376 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2377 xmldir.elements.each { |element|
2378 if %w(image video).include?(element.name) && !element.attributes['deleted']
2379 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2380 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2381 Dir[old_name + '*'].each { |file|
2382 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2383 file != new_file and sys("mv '#{file}' '#{new_file}'")
2386 if element.name == 'dir' && !element.attributes['deleted']
2387 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2388 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2389 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2393 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2397 $xmldoc.root.add_attribute('version', $VERSION)
2400 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2401 optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2402 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2404 $filename = filename
2405 select_theme(theme, limit_sizes, optimizefor32, nperrow)
2406 $default_size['thumbnails'] =~ /(.*)x(.*)/
2407 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2408 $albums_thumbnail_size =~ /(.*)x(.*)/
2409 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2411 populate_subalbums_treeview
2413 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = true
2417 def open_file_user(filename)
2418 result = open_file(filename)
2420 $config['last-opens'] ||= []
2421 if $config['last-opens'][-1] != utf8(filename)
2422 $config['last-opens'] << utf8(filename)
2424 $orig_filename = $filename
2425 tmp = Tempfile.new("boohtemp")
2428 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2430 $tempfiles << $filename << "#{$filename}.backup"
2432 $orig_filename = nil
2438 if !ask_save_modifications(utf8(_("Save this album?")),
2439 utf8(_("Do you want to save the changes to this album?")),
2440 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2443 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2445 Gtk::FileChooser::ACTION_OPEN,
2447 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2448 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2449 fc.set_current_folder(File.expand_path("~/.booh"))
2450 fc.transient_for = $main_window
2453 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2454 push_mousecursor_wait(fc)
2455 msg = open_file_user(fc.filename)
2471 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2476 def additional_booh_options
2479 options += "--mproc #{$config['mproc'].to_i} "
2481 if $config['emptycomments']
2482 options += "--empty-comments "
2488 if !ask_save_modifications(utf8(_("Save this album?")),
2489 utf8(_("Do you want to save the changes to this album?")),
2490 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2493 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2495 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2496 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2497 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2499 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2500 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2501 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2502 tbl.attach(src = Gtk::Entry.new,
2503 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2504 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2505 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2506 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2507 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2508 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2509 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2510 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2511 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2512 tbl.attach(dest = Gtk::Entry.new,
2513 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2514 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2515 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2516 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2517 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2518 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2519 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2520 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2521 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2523 tooltips = Gtk::Tooltips.new
2524 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2525 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2526 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2527 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2528 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2529 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2530 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)
2531 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2532 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2533 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2534 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2535 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)
2537 src_nb_calculated_for = ''
2539 process_src_nb = Proc.new {
2540 if src.text != src_nb_calculated_for
2541 src_nb_calculated_for = src.text
2543 Thread.kill(src_nb_thread)
2546 if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2547 if File.readable?(from_utf8(src_nb_calculated_for))
2548 src_nb_thread = Thread.new {
2549 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2550 total = { 'image' => 0, 'video' => 0, nil => 0 }
2551 `find '#{from_utf8(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2552 if File.basename(dir) =~ /^\./
2556 Dir.entries(dir.chomp).each { |file|
2557 total[entry2type(file)] += 1
2559 rescue Errno::EACCES, Errno::ENOENT
2563 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2567 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2570 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2575 timeout_src_nb = Gtk.timeout_add(100) {
2579 src_browse.signal_connect('clicked') {
2580 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2582 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2584 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2585 fc.transient_for = $main_window
2586 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2587 src.text = utf8(fc.filename)
2589 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2594 dest_browse.signal_connect('clicked') {
2595 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2597 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2599 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2600 fc.transient_for = $main_window
2601 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2602 dest.text = utf8(fc.filename)
2607 conf_browse.signal_connect('clicked') {
2608 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2610 Gtk::FileChooser::ACTION_SAVE,
2612 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2613 fc.transient_for = $main_window
2614 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2615 fc.set_current_folder(File.expand_path("~/.booh"))
2616 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2617 conf.text = utf8(fc.filename)
2624 recreate_theme_config = proc {
2625 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2627 select_theme(theme_button.label, 'all', optimize432.active?, nil)
2628 $images_size.each { |s|
2629 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2633 tooltips.set_tip(cb, utf8(s['description']), nil)
2634 theme_sizes << { :widget => cb, :value => s['name'] }
2636 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2637 tooltips = Gtk::Tooltips.new
2638 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2639 theme_sizes << { :widget => cb, :value => 'original' }
2642 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2645 $allowed_N_values.each { |n|
2647 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2649 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2651 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2655 nperrows << { :widget => rb, :value => n }
2657 nperrowradios.show_all
2659 recreate_theme_config.call
2661 theme_button.signal_connect('clicked') {
2662 if newtheme = theme_choose(theme_button.label)
2663 theme_button.label = newtheme
2664 recreate_theme_config.call
2668 dialog.vbox.add(frame1)
2669 dialog.vbox.add(frame2)
2670 dialog.window_position = Gtk::Window::POS_MOUSE
2676 dialog.run { |response|
2677 if response == Gtk::Dialog::RESPONSE_OK
2678 srcdir = from_utf8(src.text)
2679 destdir = from_utf8(dest.text)
2680 if !File.directory?(srcdir)
2681 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2683 elsif conf.text == ''
2684 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2686 elsif File.directory?(from_utf8(conf.text))
2687 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
2689 elsif destdir != make_dest_filename(destdir)
2690 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2692 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
2693 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2695 elsif File.exists?(destdir) && !File.directory?(destdir)
2696 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2698 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2699 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2701 system("mkdir '#{destdir}'")
2702 if !File.directory?(destdir)
2703 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2714 srcdir = from_utf8(src.text)
2715 destdir = from_utf8(dest.text)
2716 configskel = File.expand_path(from_utf8(conf.text))
2717 theme = theme_button.label
2718 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2719 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2720 opt432 = optimize432.active?
2721 madewith = madewithentry.text
2723 Thread.kill(src_nb_thread)
2724 gtk_thread_abandon #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2727 Gtk.timeout_remove(timeout_src_nb)
2730 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2731 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2732 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' #{additional_booh_options}",
2733 utf8(_("Please wait while scanning source directory...")),
2735 { :closure_after => proc { open_file_user(configskel) } })
2740 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2742 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2743 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2744 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2746 source = $xmldoc.root.attributes['source']
2747 dest = $xmldoc.root.attributes['destination']
2748 theme = $xmldoc.root.attributes['theme']
2749 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2750 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2751 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2753 limit_sizes = limit_sizes.split(/,/)
2755 madewith = $xmldoc.root.attributes['made-with']
2757 tooltips = Gtk::Tooltips.new
2758 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2759 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2760 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2761 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2762 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2763 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2764 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2765 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
2766 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2767 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2768 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2769 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
2770 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2772 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2773 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2774 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2775 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2776 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2777 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2778 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)
2779 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2780 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2781 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2782 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2784 madewithentry.text = madewith
2786 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)
2790 recreate_theme_config = proc {
2791 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2793 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
2795 $images_size.each { |s|
2796 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2798 if limit_sizes.include?(s['name'])
2806 tooltips.set_tip(cb, utf8(s['description']), nil)
2807 theme_sizes << { :widget => cb, :value => s['name'] }
2809 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2810 tooltips = Gtk::Tooltips.new
2811 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2812 if limit_sizes && limit_sizes.include?('original')
2815 theme_sizes << { :widget => cb, :value => 'original' }
2818 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2821 $allowed_N_values.each { |n|
2823 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2825 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2827 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2828 nperrowradios.add(Gtk::Label.new(' '))
2829 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
2832 nperrows << { :widget => rb, :value => n.to_s }
2834 nperrowradios.show_all
2836 recreate_theme_config.call
2838 theme_button.signal_connect('clicked') {
2839 if newtheme = theme_choose(theme_button.label)
2842 theme_button.label = newtheme
2843 recreate_theme_config.call
2847 dialog.vbox.add(frame1)
2848 dialog.vbox.add(frame2)
2849 dialog.window_position = Gtk::Window::POS_MOUSE
2855 dialog.run { |response|
2856 if response == Gtk::Dialog::RESPONSE_OK
2857 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2858 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2867 save_theme = theme_button.label
2868 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
2869 save_opt432 = optimize432.active?
2870 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2871 save_madewith = madewithentry.text
2874 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith)
2875 mark_document_as_dirty
2877 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
2878 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
2879 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' #{additional_booh_options}",
2880 utf8(_("Please wait while scanning source directory...")),
2882 { :closure_after => proc {
2883 open_file($filename)
2892 sel = $albums_tv.selection.selected_rows
2894 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2895 "--verbose-level #{$verbose_level} #{additional_booh_options}",
2896 utf8(_("Please wait while scanning source directory...")),
2898 { :closure_after => proc {
2899 open_file($filename)
2900 $albums_tv.selection.select_path(sel[0])
2908 sel = $albums_tv.selection.selected_rows
2910 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2911 "--verbose-level #{$verbose_level} #{additional_booh_options}",
2912 utf8(_("Please wait while scanning source directory...")),
2914 { :closure_after => proc {
2915 open_file($filename)
2916 $albums_tv.selection.select_path(sel[0])
2924 theme = $xmldoc.root.attributes['theme']
2925 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2927 limit_sizes = "--sizes #{limit_sizes}"
2929 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
2930 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
2931 utf8(_("Please wait while scanning source directory...")),
2933 { :closure_after => proc {
2934 open_file($filename)
2940 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
2942 Gtk::FileChooser::ACTION_SAVE,
2944 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2945 fc.transient_for = $main_window
2946 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2947 fc.set_current_folder(File.expand_path("~/.booh"))
2948 fc.filename = $orig_filename
2949 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2950 $orig_filename = fc.filename
2951 save_current_file_user
2957 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
2959 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2960 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2961 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2963 dialog.vbox.add(notebook = Gtk::Notebook.new)
2964 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
2965 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
2966 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2967 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
2968 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2969 tooltips = Gtk::Tooltips.new
2970 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
2971 for example: /usr/bin/mplayer %f")), nil)
2972 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
2973 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2974 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
2975 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2976 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
2977 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
2978 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
2979 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2980 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)),
2981 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2982 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)
2983 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
2984 0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2985 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)
2986 tbl.attach(emptycomments_check = Gtk::CheckButton.new(utf8(_("Use empty comments for new albums"))),
2987 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
2988 tooltips.set_tip(emptycomments_check, utf8(_("Normally, filenames are used as comments for new albums. Check this if you prefer empty comments.")), nil)
2989 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
2990 0, 2, 5, 6, Gtk::FILL, Gtk::SHRINK, 2, 2)
2991 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)
2992 smp_check.signal_connect('toggled') {
2993 if smp_check.active?
2994 smp_hbox.sensitive = true
2996 smp_hbox.sensitive = false
3000 smp_check.active = true
3001 smp_spin.value = $config['mproc'].to_i
3003 nogestures_check.active = $config['nogestures']
3004 emptycomments_check.active = $config['emptycomments']
3005 deleteondisk_check.active = $config['deleteondisk']
3007 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3008 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3009 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3010 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3011 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3013 dialog.vbox.show_all
3014 dialog.run { |response|
3015 if response == Gtk::Dialog::RESPONSE_OK
3016 $config['video-viewer'] = video_viewer_entry.text
3017 $config['browser'] = browser_entry.text
3018 if smp_check.active?
3019 $config['mproc'] = smp_spin.value.to_i
3021 $config.delete('mproc')
3023 $config['nogestures'] = nogestures_check.active?
3024 $config['emptycomments'] = emptycomments_check.active?
3025 $config['deleteondisk'] = deleteondisk_check.active?
3027 $config['convert-enhance'] = enhance_entry.text
3034 if $undo_tb.sensitive?
3035 $redo_tb.sensitive = $redo_mb.sensitive = true
3036 if not more_undoes = UndoHandler.undo($statusbar)
3037 $undo_tb.sensitive = $undo_mb.sensitive = false
3043 if $redo_tb.sensitive?
3044 $undo_tb.sensitive = $undo_mb.sensitive = true
3045 if not more_redoes = UndoHandler.redo($statusbar)
3046 $redo_tb.sensitive = $redo_mb.sensitive = false
3051 def show_one_click_explanation(intro)
3052 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
3054 %s When such a tool is activated
3055 (<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
3056 on a thumbnail will immediately apply the desired action.
3058 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
3064 GNU GENERAL PUBLIC LICENSE
3065 Version 2, June 1991
3067 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
3068 675 Mass Ave, Cambridge, MA 02139, USA
3069 Everyone is permitted to copy and distribute verbatim copies
3070 of this license document, but changing it is not allowed.
3074 The licenses for most software are designed to take away your
3075 freedom to share and change it. By contrast, the GNU General Public
3076 License is intended to guarantee your freedom to share and change free
3077 software--to make sure the software is free for all its users. This
3078 General Public License applies to most of the Free Software
3079 Foundation's software and to any other program whose authors commit to
3080 using it. (Some other Free Software Foundation software is covered by
3081 the GNU Library General Public License instead.) You can apply it to
3084 When we speak of free software, we are referring to freedom, not
3085 price. Our General Public Licenses are designed to make sure that you
3086 have the freedom to distribute copies of free software (and charge for
3087 this service if you wish), that you receive source code or can get it
3088 if you want it, that you can change the software or use pieces of it
3089 in new free programs; and that you know you can do these things.
3091 To protect your rights, we need to make restrictions that forbid
3092 anyone to deny you these rights or to ask you to surrender the rights.
3093 These restrictions translate to certain responsibilities for you if you
3094 distribute copies of the software, or if you modify it.
3096 For example, if you distribute copies of such a program, whether
3097 gratis or for a fee, you must give the recipients all the rights that
3098 you have. You must make sure that they, too, receive or can get the
3099 source code. And you must show them these terms so they know their
3102 We protect your rights with two steps: (1) copyright the software, and
3103 (2) offer you this license which gives you legal permission to copy,
3104 distribute and/or modify the software.
3106 Also, for each author's protection and ours, we want to make certain
3107 that everyone understands that there is no warranty for this free
3108 software. If the software is modified by someone else and passed on, we
3109 want its recipients to know that what they have is not the original, so
3110 that any problems introduced by others will not reflect on the original
3111 authors' reputations.
3113 Finally, any free program is threatened constantly by software
3114 patents. We wish to avoid the danger that redistributors of a free
3115 program will individually obtain patent licenses, in effect making the
3116 program proprietary. To prevent this, we have made it clear that any
3117 patent must be licensed for everyone's free use or not licensed at all.
3119 The precise terms and conditions for copying, distribution and
3120 modification follow.
3123 GNU GENERAL PUBLIC LICENSE
3124 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
3126 0. This License applies to any program or other work which contains
3127 a notice placed by the copyright holder saying it may be distributed
3128 under the terms of this General Public License. The "Program", below,
3129 refers to any such program or work, and a "work based on the Program"
3130 means either the Program or any derivative work under copyright law:
3131 that is to say, a work containing the Program or a portion of it,
3132 either verbatim or with modifications and/or translated into another
3133 language. (Hereinafter, translation is included without limitation in
3134 the term "modification".) Each licensee is addressed as "you".
3136 Activities other than copying, distribution and modification are not
3137 covered by this License; they are outside its scope. The act of
3138 running the Program is not restricted, and the output from the Program
3139 is covered only if its contents constitute a work based on the
3140 Program (independent of having been made by running the Program).
3141 Whether that is true depends on what the Program does.
3143 1. You may copy and distribute verbatim copies of the Program's
3144 source code as you receive it, in any medium, provided that you
3145 conspicuously and appropriately publish on each copy an appropriate
3146 copyright notice and disclaimer of warranty; keep intact all the
3147 notices that refer to this License and to the absence of any warranty;
3148 and give any other recipients of the Program a copy of this License
3149 along with the Program.
3151 You may charge a fee for the physical act of transferring a copy, and
3152 you may at your option offer warranty protection in exchange for a fee.
3154 2. You may modify your copy or copies of the Program or any portion
3155 of it, thus forming a work based on the Program, and copy and
3156 distribute such modifications or work under the terms of Section 1
3157 above, provided that you also meet all of these conditions:
3159 a) You must cause the modified files to carry prominent notices
3160 stating that you changed the files and the date of any change.
3162 b) You must cause any work that you distribute or publish, that in
3163 whole or in part contains or is derived from the Program or any
3164 part thereof, to be licensed as a whole at no charge to all third
3165 parties under the terms of this License.
3167 c) If the modified program normally reads commands interactively
3168 when run, you must cause it, when started running for such
3169 interactive use in the most ordinary way, to print or display an
3170 announcement including an appropriate copyright notice and a
3171 notice that there is no warranty (or else, saying that you provide
3172 a warranty) and that users may redistribute the program under
3173 these conditions, and telling the user how to view a copy of this
3174 License. (Exception: if the Program itself is interactive but
3175 does not normally print such an announcement, your work based on
3176 the Program is not required to print an announcement.)
3179 These requirements apply to the modified work as a whole. If
3180 identifiable sections of that work are not derived from the Program,
3181 and can be reasonably considered independent and separate works in
3182 themselves, then this License, and its terms, do not apply to those
3183 sections when you distribute them as separate works. But when you
3184 distribute the same sections as part of a whole which is a work based
3185 on the Program, the distribution of the whole must be on the terms of
3186 this License, whose permissions for other licensees extend to the
3187 entire whole, and thus to each and every part regardless of who wrote it.
3189 Thus, it is not the intent of this section to claim rights or contest
3190 your rights to work written entirely by you; rather, the intent is to
3191 exercise the right to control the distribution of derivative or
3192 collective works based on the Program.
3194 In addition, mere aggregation of another work not based on the Program
3195 with the Program (or with a work based on the Program) on a volume of
3196 a storage or distribution medium does not bring the other work under
3197 the scope of this License.
3199 3. You may copy and distribute the Program (or a work based on it,
3200 under Section 2) in object code or executable form under the terms of
3201 Sections 1 and 2 above provided that you also do one of the following:
3203 a) Accompany it with the complete corresponding machine-readable
3204 source code, which must be distributed under the terms of Sections
3205 1 and 2 above on a medium customarily used for software interchange; or,
3207 b) Accompany it with a written offer, valid for at least three
3208 years, to give any third party, for a charge no more than your
3209 cost of physically performing source distribution, a complete
3210 machine-readable copy of the corresponding source code, to be
3211 distributed under the terms of Sections 1 and 2 above on a medium
3212 customarily used for software interchange; or,
3214 c) Accompany it with the information you received as to the offer
3215 to distribute corresponding source code. (This alternative is
3216 allowed only for noncommercial distribution and only if you
3217 received the program in object code or executable form with such
3218 an offer, in accord with Subsection b above.)
3220 The source code for a work means the preferred form of the work for
3221 making modifications to it. For an executable work, complete source
3222 code means all the source code for all modules it contains, plus any
3223 associated interface definition files, plus the scripts used to
3224 control compilation and installation of the executable. However, as a
3225 special exception, the source code distributed need not include
3226 anything that is normally distributed (in either source or binary
3227 form) with the major components (compiler, kernel, and so on) of the
3228 operating system on which the executable runs, unless that component
3229 itself accompanies the executable.
3231 If distribution of executable or object code is made by offering
3232 access to copy from a designated place, then offering equivalent
3233 access to copy the source code from the same place counts as
3234 distribution of the source code, even though third parties are not
3235 compelled to copy the source along with the object code.
3238 4. You may not copy, modify, sublicense, or distribute the Program
3239 except as expressly provided under this License. Any attempt
3240 otherwise to copy, modify, sublicense or distribute the Program is
3241 void, and will automatically terminate your rights under this License.
3242 However, parties who have received copies, or rights, from you under
3243 this License will not have their licenses terminated so long as such
3244 parties remain in full compliance.
3246 5. You are not required to accept this License, since you have not
3247 signed it. However, nothing else grants you permission to modify or
3248 distribute the Program or its derivative works. These actions are
3249 prohibited by law if you do not accept this License. Therefore, by
3250 modifying or distributing the Program (or any work based on the
3251 Program), you indicate your acceptance of this License to do so, and
3252 all its terms and conditions for copying, distributing or modifying
3253 the Program or works based on it.
3255 6. Each time you redistribute the Program (or any work based on the
3256 Program), the recipient automatically receives a license from the
3257 original licensor to copy, distribute or modify the Program subject to
3258 these terms and conditions. You may not impose any further
3259 restrictions on the recipients' exercise of the rights granted herein.
3260 You are not responsible for enforcing compliance by third parties to
3263 7. If, as a consequence of a court judgment or allegation of patent
3264 infringement or for any other reason (not limited to patent issues),
3265 conditions are imposed on you (whether by court order, agreement or
3266 otherwise) that contradict the conditions of this License, they do not
3267 excuse you from the conditions of this License. If you cannot
3268 distribute so as to satisfy simultaneously your obligations under this
3269 License and any other pertinent obligations, then as a consequence you
3270 may not distribute the Program at all. For example, if a patent
3271 license would not permit royalty-free redistribution of the Program by
3272 all those who receive copies directly or indirectly through you, then
3273 the only way you could satisfy both it and this License would be to
3274 refrain entirely from distribution of the Program.
3276 If any portion of this section is held invalid or unenforceable under
3277 any particular circumstance, the balance of the section is intended to
3278 apply and the section as a whole is intended to apply in other
3281 It is not the purpose of this section to induce you to infringe any
3282 patents or other property right claims or to contest validity of any
3283 such claims; this section has the sole purpose of protecting the
3284 integrity of the free software distribution system, which is
3285 implemented by public license practices. Many people have made
3286 generous contributions to the wide range of software distributed
3287 through that system in reliance on consistent application of that
3288 system; it is up to the author/donor to decide if he or she is willing
3289 to distribute software through any other system and a licensee cannot
3292 This section is intended to make thoroughly clear what is believed to
3293 be a consequence of the rest of this License.
3296 8. If the distribution and/or use of the Program is restricted in
3297 certain countries either by patents or by copyrighted interfaces, the
3298 original copyright holder who places the Program under this License
3299 may add an explicit geographical distribution limitation excluding
3300 those countries, so that distribution is permitted only in or among
3301 countries not thus excluded. In such case, this License incorporates
3302 the limitation as if written in the body of this License.
3304 9. The Free Software Foundation may publish revised and/or new versions
3305 of the General Public License from time to time. Such new versions will
3306 be similar in spirit to the present version, but may differ in detail to
3307 address new problems or concerns.
3309 Each version is given a distinguishing version number. If the Program
3310 specifies a version number of this License which applies to it and "any
3311 later version", you have the option of following the terms and conditions
3312 either of that version or of any later version published by the Free
3313 Software Foundation. If the Program does not specify a version number of
3314 this License, you may choose any version ever published by the Free Software
3317 10. If you wish to incorporate parts of the Program into other free
3318 programs whose distribution conditions are different, write to the author
3319 to ask for permission. For software which is copyrighted by the Free
3320 Software Foundation, write to the Free Software Foundation; we sometimes
3321 make exceptions for this. Our decision will be guided by the two goals
3322 of preserving the free status of all derivatives of our free software and
3323 of promoting the sharing and reuse of software generally.
3327 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
3328 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
3329 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
3330 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
3331 OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
3332 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
3333 TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
3334 PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
3335 REPAIR OR CORRECTION.
3337 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
3338 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
3339 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
3340 INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
3341 OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
3342 TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
3343 YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
3344 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
3345 POSSIBILITY OF SUCH DAMAGES.
3349 def create_menu_and_toolbar
3352 mb = Gtk::MenuBar.new
3354 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
3355 filesubmenu = Gtk::Menu.new
3356 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
3357 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
3358 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3359 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
3360 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
3361 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3362 tooltips = Gtk::Tooltips.new
3363 filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
3364 $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3365 tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
3366 filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
3367 $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3368 tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
3369 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
3370 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3371 tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums (anywhere)")), nil)
3372 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3373 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
3374 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3375 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
3376 filesubmenu.append($view_wa = Gtk::ImageMenuItem.new(utf8(_("View web-album with browser"))).set_sensitive(false))
3377 $view_wa.image = Gtk::Image.new("#{$FPATH}/images/stock-view-webalbum-16.png")
3378 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3379 filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
3380 tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
3381 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3382 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
3383 filemenu.set_submenu(filesubmenu)
3386 new.signal_connect('activate') { new_album }
3387 open.signal_connect('activate') { open_file_popup }
3388 $save.signal_connect('activate') { save_current_file_user }
3389 $save_as.signal_connect('activate') { save_as_do }
3390 $merge_current.signal_connect('activate') { merge_current }
3391 $merge_newsubs.signal_connect('activate') { merge_newsubs }
3392 $merge.signal_connect('activate') { merge }
3393 $generate.signal_connect('activate') {
3395 call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
3396 utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
3398 { :successmsg => utf8(_("Your web-album is now ready in directory '%s'.
3399 Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
3400 :successmsg_linkurl => $xmldoc.root.attributes['destination'],
3401 :closure_after => proc {
3402 $xmldoc.elements.each('//dir') { |elem|
3403 elem.add_attribute('already-generated', 'true')
3405 UndoHandler.cleanup #- prevent save_changes to mark current dir as not already generated
3406 $undo_tb.sensitive = $undo_mb.sensitive = false
3407 $redo_tb.sensitive = $redo_mb.sensitive = false
3409 $generated_outofline = true
3413 $view_wa.signal_connect('activate') {
3414 indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
3415 if File.exists?(indexhtml)
3418 show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
3421 $properties.signal_connect('activate') { properties }
3423 quit.signal_connect('activate') { try_quit }
3425 editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
3426 editsubmenu = Gtk::Menu.new
3427 editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
3428 editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
3429 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3430 editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
3431 $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
3432 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)
3433 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3434 editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
3435 editmenu.set_submenu(editsubmenu)
3438 $remove_all_captions.signal_connect('activate') { remove_all_captions }
3440 prefs.signal_connect('activate') { preferences }
3442 helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
3443 helpsubmenu = Gtk::Menu.new
3444 helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
3445 one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
3446 helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
3447 speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
3448 helpsubmenu.append(tutos = Gtk::ImageMenuItem.new(utf8(_("Online tutorials (opens a web-browser)"))))
3449 tutos.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3450 helpsubmenu.append( Gtk::SeparatorMenuItem.new)
3451 helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
3452 helpmenu.set_submenu(helpsubmenu)
3455 one_click.signal_connect('activate') {
3456 show_one_click_explanation(_("One-Click tools are available in the toolbar."))
3459 speed.signal_connect('activate') {
3460 show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
3462 <span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
3463 <span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
3464 <span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
3465 <span foreground='darkblue'>Control-Enter</span>: for an image, open larger view; for a video, launch player
3466 <span foreground='darkblue'>Control-Delete</span>: delete image
3467 <span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
3468 <span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
3469 <span foreground='darkblue'>Control-z</span>: undo
3470 <span foreground='darkblue'>Control-r</span>: redo
3472 <span size='large' weight='bold'>Mouse gestures:</span>
3474 Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
3475 for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
3477 <span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
3478 <span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
3479 <span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
3480 <span foreground='darkblue'>Left click, hold left button, right click</span>: undo
3481 <span foreground='darkblue'>Right click, hold right button, left click</span>: redo
3482 ")), { :pos_centered => true, :not_transient => true })
3485 tutos.signal_connect('activate') {
3486 open_url('http://zarb.org/~gc/html/booh/tutorial.html')
3489 about.signal_connect('activate') {
3490 Gtk::AboutDialog.set_url_hook { |dialog, url| open_url(url) }
3491 Gtk::AboutDialog.show($main_window, { :name => 'booh',
3492 :version => $VERSION,
3493 :copyright => 'Copyright (c) 2005 Guillaume Cottenceau',
3494 :license => get_license,
3495 :website => 'http://zarb.org/~gc/html/booh.html',
3496 :authors => [ 'Guillaume Cottenceau' ],
3497 :artists => [ 'Ayo73' ],
3498 :comments => utf8(_("''The Web-Album of choice for discriminating Linux users''")),
3499 :translator_credits => utf8(_('Japanese: Masao Mutoh
3500 German: Roland Eckert
3501 French: Guillaume Cottenceau')),
3502 :logo => Gdk::Pixbuf.new("#{$FPATH}/images/logo.png") })