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 puts _("Usage: %s [OPTION]...") % File.basename($0)
55 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
60 parser = GetoptLong.new
61 parser.set_options(*$options.collect { |ary| ary[0..2] })
63 parser.each_option do |name, arg|
69 when '--verbose-level'
70 $verbose_level = arg.to_i
83 $config_file = File.expand_path('~/.booh-gui-rc')
84 if File.readable?($config_file)
85 $xmldoc = REXML::Document.new(File.new($config_file))
86 $xmldoc.root.elements.each { |element|
87 txt = element.get_text
89 if txt.value =~ /~~~/ || element.name == 'last-opens'
90 $config[element.name] = txt.value.split(/~~~/)
92 $config[element.name] = txt.value
94 elsif element.elements.size == 0
95 $config[element.name] = ''
97 $config[element.name] = {}
100 $config[element.name][chld.name] = txt ? txt.value : nil
105 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
106 $config['browser'] ||= '/usr/bin/mozilla-firefox %f'
107 if !FileTest.directory?(File.expand_path('~/.booh'))
108 system("mkdir ~/.booh")
116 if !system("which convert >/dev/null 2>/dev/null")
117 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
118 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
121 if !system("which identify >/dev/null 2>/dev/null")
122 show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
123 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
125 missing = %w(transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
127 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
130 viewer_binary = $config['video-viewer'].split.first
131 if viewer_binary && !File.executable?(viewer_binary)
132 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
133 You should fix this in Edit/Preferences so that you can view videos.
135 Problem was: '%s' is not an executable file.") % viewer_binary), { :pos_centered => true, :not_transient => true })
137 browser_binary = $config['browser'].split.first
138 if browser_binary && !File.executable?(browser_binary)
139 show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
140 You should fix this in Edit/Preferences so that you can open URLs.
142 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
147 if $config['last-opens'] && $config['last-opens'].size > 5
148 $config['last-opens'] = $config['last-opens'][-5, 5]
151 ios = File.open($config_file, "w")
152 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
153 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
154 $config.each_pair { |key, value|
155 elem = $xmldoc.root.add_element key
157 $config[key].each_pair { |subkey, subvalue|
158 subelem = elem.add_element subkey
159 subelem.add_text subvalue.to_s
161 elsif value.is_a? Array
162 elem.add_text value.join('~~~')
167 elem.add_text value.to_s
171 $xmldoc.write(ios, 0)
174 $tempfiles.each { |f|
179 def set_mousecursor(what, *widget)
180 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
181 if widget[0] && widget[0].window
182 widget[0].window.cursor = cursor
184 if $main_window.window
185 $main_window.window.cursor = cursor
187 $current_cursor = what
189 def set_mousecursor_wait(*widget)
190 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
191 if Thread.current == Thread.main
192 Gtk.main_iteration while Gtk.events_pending?
195 def set_mousecursor_normal(*widget)
196 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
198 def push_mousecursor_wait(*widget)
199 if $current_cursor != Gdk::Cursor::WATCH
200 $save_cursor = $current_cursor
201 gtk_thread_protect { set_mousecursor_wait(*widget) }
204 def pop_mousecursor(*widget)
205 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
209 source = $xmldoc.root.attributes['source']
210 dest = $xmldoc.root.attributes['destination']
211 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
214 def full_src_dir_to_rel(path, source)
215 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
218 def build_full_dest_filename(filename)
219 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
222 def save_undo(name, closure, *params)
223 UndoHandler.save_undo(name, closure, [ *params ])
224 $undo_tb.sensitive = $undo_mb.sensitive = true
225 $redo_tb.sensitive = $redo_mb.sensitive = false
228 def view_element(filename, closures)
229 if entry2type(filename) == 'video'
230 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
236 w = Gtk::Window.new.set_title(filename)
238 msg 3, "filename: #{filename}"
239 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
240 #- typically this file won't exist in case of videos; try with the largest thumbnail around
241 if !File.exists?(dest_img)
242 if entry2type(filename) == 'video'
243 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
244 if not alternatives.empty?
245 dest_img = alternatives[-1]
248 push_mousecursor_wait
249 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
251 if !File.exists?(dest_img)
252 msg 2, _("Could not generate fullscreen thumbnail!")
257 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)))
258 evt.signal_connect('button-press-event') { |this, event|
259 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
260 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
262 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
264 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
265 delete_item.signal_connect('activate') {
267 closures[:delete].call
270 menu.popup(nil, nil, event.button, event.time)
273 evt.signal_connect('button-release-event') { |this, event|
275 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
276 msg 3, "gesture delete: click-drag right button to the bottom"
278 closures[:delete].call
279 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
283 tooltips = Gtk::Tooltips.new
284 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
286 w.signal_connect('key-press-event') { |w,event|
287 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
289 closures[:delete].call
293 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
294 b.signal_connect('clicked') { w.destroy }
297 vb.pack_start(evt, false, false)
298 vb.pack_end(bottom, false, false)
301 w.signal_connect('delete-event') { w.destroy }
302 w.window_position = Gtk::Window::POS_CENTER
306 def scroll_upper(scrolledwindow, ypos_top)
307 newval = scrolledwindow.vadjustment.value -
308 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
309 if newval < scrolledwindow.vadjustment.lower
310 newval = scrolledwindow.vadjustment.lower
312 scrolledwindow.vadjustment.value = newval
315 def scroll_lower(scrolledwindow, ypos_bottom)
316 newval = scrolledwindow.vadjustment.value +
317 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
318 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
319 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
321 scrolledwindow.vadjustment.value = newval
324 def autoscroll_if_needed(scrolledwindow, image, textview)
325 #- autoscroll if cursor or image is not visible, if possible
326 if image && image.window || textview.window
327 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
328 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
329 current_miny_visible = scrolledwindow.vadjustment.value
330 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
331 if ypos_top < current_miny_visible
332 scroll_upper(scrolledwindow, ypos_top)
333 elsif ypos_bottom > current_maxy_visible
334 scroll_lower(scrolledwindow, ypos_bottom)
339 def create_editzone(scrolledwindow, pagenum, image)
340 frame = Gtk::Frame.new
341 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
342 frame.set_shadow_type(Gtk::SHADOW_IN)
343 textview.signal_connect('key-press-event') { |w, event|
344 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
345 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
346 scrolledwindow.signal_emit('key-press-event', event)
348 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
349 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
350 if event.keyval == Gdk::Keyval::GDK_Up
351 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
352 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
354 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
357 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
358 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
360 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
366 textview.signal_connect('focus-in-event') { |w, event|
367 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
371 candidate_undo_text = nil
372 textview.signal_connect('focus-in-event') { |w, event|
373 candidate_undo_text = textview.buffer.text
376 textview.signal_connect('key-release-event') { |w, event|
377 if candidate_undo_text && candidate_undo_text != textview.buffer.text
379 save_undo(_("text edit"),
381 save_text = textview.buffer.text
382 textview.buffer.text = text
384 $notebook.set_page(pagenum)
386 textview.buffer.text = save_text
388 $notebook.set_page(pagenum)
390 }, candidate_undo_text)
391 candidate_undo_text = nil
394 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)
395 autoscroll_if_needed(scrolledwindow, image, textview)
400 return [ frame, textview ]
403 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
405 if !$modified_pixbufs[thumbnail_img]
406 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
407 elsif !$modified_pixbufs[thumbnail_img][:orig]
408 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
411 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
414 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
415 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
416 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
417 if pixbuf.height > desired_y
418 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
419 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
420 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
425 if $modified_pixbufs[thumbnail_img][:whitebalance]
426 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
429 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
432 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
435 #- update rotate attribute
436 xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
438 $modified_pixbufs[thumbnail_img] ||= {}
439 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
440 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
442 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
445 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
448 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
450 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
452 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
453 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
455 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
456 $notebook.set_page(0)
457 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
462 def color_swap(xmldir, attributes_prefix)
464 if xmldir.attributes["#{attributes_prefix}color-swap"]
465 xmldir.delete_attribute("#{attributes_prefix}color-swap")
467 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
471 def enhance(xmldir, attributes_prefix)
473 if xmldir.attributes["#{attributes_prefix}enhance"]
474 xmldir.delete_attribute("#{attributes_prefix}enhance")
476 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
480 def change_frame_offset(xmldir, attributes_prefix, value)
482 xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
485 def ask_new_frame_offset(xmldir, attributes_prefix)
487 value = xmldir.attributes["#{attributes_prefix}frame-offset"]
492 dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
494 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
495 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
496 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
500 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
501 from. There are approximately 25 frames per second in a video.
504 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
505 entry.signal_connect('key-press-event') { |w, event|
506 if event.keyval == Gdk::Keyval::GDK_Return
507 dialog.response(Gtk::Dialog::RESPONSE_OK)
509 elsif event.keyval == Gdk::Keyval::GDK_Escape
510 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
513 false #- propagate if needed
517 dialog.window_position = Gtk::Window::POS_MOUSE
520 dialog.run { |response|
523 if response == Gtk::Dialog::RESPONSE_OK
525 msg 3, "changing frame offset to #{newval}"
526 return { :old => value, :new => newval }
533 def change_whitebalance(xmlelem, attributes_prefix, value)
535 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
538 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
540 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
541 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
542 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
543 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
544 destfile = "#{thumbnail_img}-orig-whitebalance.jpg"
545 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
546 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
547 $modified_pixbufs[thumbnail_img] ||= {}
548 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
549 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
550 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
553 $modified_pixbufs[thumbnail_img] ||= {}
554 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
556 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
559 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
560 #- init $modified_pixbufs correctly
561 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
563 value = xmlelem.attributes["#{attributes_prefix}white-balance"] || "0"
565 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
567 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
568 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
569 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
573 _("You can fix the <b>white balance</b> of the image, if your image is too blue
574 or too yellow because your camera didn't detect the light correctly. Drag the
575 slider below the image to the left for more blue, to the right for more yellow.
578 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
579 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
581 dialog.window_position = Gtk::Window::POS_MOUSE
585 timeout = Gtk.timeout_add(100) {
586 if hs.value != lastval
588 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
593 dialog.run { |response|
594 Gtk.timeout_remove(timeout)
595 if response == Gtk::Dialog::RESPONSE_OK
597 newval = hs.value.to_s
598 msg 3, "changing white balance to #{newval}"
600 return { :old => value, :new => newval }
602 $modified_pixbufs[thumbnail_img] ||= {}
603 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
604 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
611 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
612 system("rm -f '#{destfile}'")
613 #- type can be 'element' or 'subdir'
615 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
617 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
621 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
623 push_mousecursor_wait
624 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
627 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
633 def popup_thumbnail_menu(event, optionals, type, xmldir, attributes_prefix, possible_actions, closures)
634 distribute_multiple_call = Proc.new { |action, arg|
635 $selected_elements.each_key { |path|
636 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
638 if possible_actions[:can_multiple] && $selected_elements.length > 0
639 UndoHandler.begin_batch
640 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
641 UndoHandler.end_batch
643 closures[action].call(arg)
645 $selected_elements = {}
648 if optionals.include?('change_image')
649 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
650 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
651 changeimg.signal_connect('activate') { closures[:change].call }
652 menu.append( Gtk::SeparatorMenuItem.new)
656 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
657 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
658 view.signal_connect('activate') { closures[:view].call }
659 menu.append( Gtk::SeparatorMenuItem.new)
661 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
662 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
663 view.signal_connect('activate') { closures[:view].call }
664 menu.append( Gtk::SeparatorMenuItem.new)
667 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
668 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
669 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
670 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
671 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
672 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
673 if !possible_actions[:can_multiple] || $selected_elements.length == 0
674 menu.append( Gtk::SeparatorMenuItem.new)
675 if !possible_actions[:forbid_left]
676 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
677 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
678 moveleft.signal_connect('activate') { closures[:move].call('left') }
679 if !possible_actions[:can_left]
680 moveleft.sensitive = false
683 if !possible_actions[:forbid_right]
684 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
685 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
686 moveright.signal_connect('activate') { closures[:move].call('right') }
687 if !possible_actions[:can_right]
688 moveright.sensitive = false
691 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
692 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
693 moveup.signal_connect('activate') { closures[:move].call('up') }
694 if !possible_actions[:can_up]
695 moveup.sensitive = false
697 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
698 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
699 movedown.signal_connect('activate') { closures[:move].call('down') }
700 if !possible_actions[:can_down]
701 movedown.sensitive = false
705 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
706 menu.append( Gtk::SeparatorMenuItem.new)
707 menu.append( color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
708 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
709 color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
710 menu.append( flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
711 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
712 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
713 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
714 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
715 frame_offset.signal_connect('activate') {
716 if possible_actions[:can_multiple] && $selected_elements.length > 0
717 if values = ask_new_frame_offset(nil, '')
718 distribute_multiple_call.call(:frame_offset, values)
721 closures[:frame_offset].call
726 menu.append( Gtk::SeparatorMenuItem.new)
727 if !possible_actions[:can_multiple] || $selected_elements.length == 0
728 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
729 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
730 whitebalance.signal_connect('activate') { closures[:whitebalance].call }
732 if !possible_actions[:can_multiple] || $selected_elements.length == 0
733 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
734 _("Enhance constrast"))))
736 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
738 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
739 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
740 if optionals.include?('delete')
741 menu.append( Gtk::SeparatorMenuItem.new)
742 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
743 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
744 if !possible_actions[:can_multiple] || $selected_elements.length == 0
745 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
746 paste_item.signal_connect('activate') { closures[:paste].call }
747 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
748 clear_item.signal_connect('activate') { $cuts = [] }
750 paste_item.sensitive = clear_item.sensitive = false
753 menu.append( Gtk::SeparatorMenuItem.new)
754 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
755 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
758 menu.popup(nil, nil, event.button, event.time)
761 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
764 frame1 = Gtk::Frame.new
765 fullpath = from_utf8("#{$current_path}/#{filename}")
767 my_gen_real_thumbnail = proc {
768 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
771 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
772 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
773 frame1.add(img = Gtk::Image.new)
774 my_gen_real_thumbnail.call
776 frame1.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
778 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
780 tooltips = Gtk::Tooltips.new
781 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
782 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
784 frame2, textview = create_editzone($autotable_sw, 1, img)
785 textview.buffer.text = utf8(caption)
786 textview.set_justification(Gtk::Justification::CENTER)
788 vbox = Gtk::VBox.new(false, 5)
789 vbox.pack_start(evtbox, false, false)
790 vbox.pack_start(frame2, false, false)
791 autotable.append(vbox, filename)
793 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
794 $vbox2widgets[vbox] = { :textview => textview, :image => img }
796 #- to be able to find widgets by name
797 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
799 cleanup_all_thumbnails = Proc.new {
800 #- remove out of sync images
801 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
802 for sizeobj in $images_size
803 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
808 rotate_and_cleanup = Proc.new { |angle|
809 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
810 cleanup_all_thumbnails.call
813 move = Proc.new { |direction|
814 do_method = "move_#{direction}"
815 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
817 done = autotable.method(do_method).call(vbox)
818 textview.grab_focus #- because if moving, focus is stolen
822 save_undo(_("move %s") % direction,
824 autotable.method(undo_method).call(vbox)
825 textview.grab_focus #- because if moving, focus is stolen
826 autoscroll_if_needed($autotable_sw, img, textview)
827 $notebook.set_page(1)
829 autotable.method(do_method).call(vbox)
830 textview.grab_focus #- because if moving, focus is stolen
831 autoscroll_if_needed($autotable_sw, img, textview)
832 $notebook.set_page(1)
838 color_swap_and_cleanup = Proc.new {
839 perform_color_swap_and_cleanup = Proc.new {
840 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
841 my_gen_real_thumbnail.call
844 cleanup_all_thumbnails.call
845 perform_color_swap_and_cleanup.call
847 save_undo(_("color swap"),
849 perform_color_swap_and_cleanup.call
851 autoscroll_if_needed($autotable_sw, img, textview)
852 $notebook.set_page(1)
854 perform_color_swap_and_cleanup.call
856 autoscroll_if_needed($autotable_sw, img, textview)
857 $notebook.set_page(1)
862 change_frame_offset_and_cleanup_real = Proc.new { |values|
863 perform_change_frame_offset_and_cleanup = Proc.new { |val|
864 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
865 my_gen_real_thumbnail.call
867 perform_change_frame_offset_and_cleanup.call(values[:new])
869 save_undo(_("specify frame offset"),
871 perform_change_frame_offset_and_cleanup.call(values[:old])
873 autoscroll_if_needed($autotable_sw, img, textview)
874 $notebook.set_page(1)
876 perform_change_frame_offset_and_cleanup.call(values[:new])
878 autoscroll_if_needed($autotable_sw, img, textview)
879 $notebook.set_page(1)
884 change_frame_offset_and_cleanup = Proc.new {
885 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
886 change_frame_offset_and_cleanup_real.call(values)
890 whitebalance_and_cleanup = Proc.new {
891 if values = ask_whitebalance(fullpath, thumbnail_img, img,
892 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
893 perform_change_whitebalance_and_cleanup = Proc.new { |val|
894 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
895 recalc_whitebalance(val, fullpath, thumbnail_img, img,
896 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
897 cleanup_all_thumbnails.call
899 perform_change_whitebalance_and_cleanup.call(values[:new])
901 save_undo(_("fix white balance"),
903 perform_change_whitebalance_and_cleanup.call(values[:old])
905 autoscroll_if_needed($autotable_sw, img, textview)
906 $notebook.set_page(1)
908 perform_change_whitebalance_and_cleanup.call(values[:new])
910 autoscroll_if_needed($autotable_sw, img, textview)
911 $notebook.set_page(1)
917 enhance_and_cleanup = Proc.new {
918 perform_enhance_and_cleanup = Proc.new {
919 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
920 my_gen_real_thumbnail.call
923 cleanup_all_thumbnails.call
924 perform_enhance_and_cleanup.call
926 save_undo(_("enhance"),
928 perform_enhance_and_cleanup.call
930 autoscroll_if_needed($autotable_sw, img, textview)
931 $notebook.set_page(1)
933 perform_enhance_and_cleanup.call
935 autoscroll_if_needed($autotable_sw, img, textview)
936 $notebook.set_page(1)
941 delete = Proc.new { |isacut|
942 if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed?")), { :okcancel => true })
945 perform_delete = Proc.new {
946 after = autotable.get_next_widget(vbox)
948 after = autotable.get_previous_widget(vbox)
950 if $config['deleteondisk'] && !isacut
951 msg 3, "scheduling for delete: #{fullpath}"
952 $todelete << fullpath
954 autotable.remove(vbox)
956 $vbox2widgets[after][:textview].grab_focus
957 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
961 previous_pos = autotable.get_current_number(vbox)
965 if $xmldir.child_byname_notattr('dir', 'deleted')
966 $xmldir.delete_attribute('thumbnails-caption')
967 $xmldir.delete_attribute('thumbnails-captionfile')
969 $xmldir.add_attribute('deleted', 'true')
971 while moveup.parent.name == 'dir'
972 moveup = moveup.parent
973 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
974 moveup.add_attribute('deleted', 'true')
980 save_changes('forced')
981 populate_subalbums_treeview
983 save_undo(_("delete"),
985 autotable.reinsert(pos, vbox, filename)
986 $notebook.set_page(1)
987 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
989 msg 3, "removing deletion schedule of: #{fullpath}"
990 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
993 $notebook.set_page(1)
1002 $cuts << { :vbox => vbox, :filename => filename }
1003 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1008 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1011 autotable.queue_draws << proc {
1012 $vbox2widgets[last[:vbox]][:textview].grab_focus
1013 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1015 save_undo(_("paste"),
1017 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1018 $notebook.set_page(1)
1021 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1023 $notebook.set_page(1)
1026 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1031 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1032 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real }
1034 textview.signal_connect('key-press-event') { |w, event|
1037 x, y = autotable.get_current_pos(vbox)
1038 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1039 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1040 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1041 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1043 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1044 $vbox2widgets[widget_up][:textview].grab_focus
1051 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1053 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1054 $vbox2widgets[widget_down][:textview].grab_focus
1061 if event.keyval == Gdk::Keyval::GDK_Left
1064 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1071 rotate_and_cleanup.call(-90)
1074 if event.keyval == Gdk::Keyval::GDK_Right
1075 next_ = autotable.get_next_widget(vbox)
1076 if next_ && autotable.get_current_pos(next_)[0] > x
1078 $vbox2widgets[next_][:textview].grab_focus
1085 rotate_and_cleanup.call(90)
1088 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1091 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1092 view_element(filename, { :delete => delete })
1095 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1098 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1102 !propagate #- propagate if needed
1105 $ignore_next_release = false
1106 evtbox.signal_connect('button-press-event') { |w, event|
1107 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1108 if event.state & Gdk::Window::BUTTON3_MASK != 0
1109 #- gesture redo: hold right mouse button then click left mouse button
1110 $config['nogestures'] or perform_redo
1111 $ignore_next_release = true
1113 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1115 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1117 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1118 elsif $enhance.active?
1119 enhance_and_cleanup.call
1120 elsif $delete.active?
1124 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1127 $button1_pressed_autotable = true
1128 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1129 if event.state & Gdk::Window::BUTTON1_MASK != 0
1130 #- gesture undo: hold left mouse button then click right mouse button
1131 $config['nogestures'] or perform_undo
1132 $ignore_next_release = true
1134 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1135 view_element(filename, { :delete => delete })
1140 evtbox.signal_connect('button-release-event') { |w, event|
1141 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1142 if !$ignore_next_release
1143 x, y = autotable.get_current_pos(vbox)
1144 next_ = autotable.get_next_widget(vbox)
1145 popup_thumbnail_menu(event, ['delete'], type, $xmldir.elements["*[@filename='#{filename}']"], '',
1146 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1147 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true },
1148 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1149 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1150 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) } })
1152 $ignore_next_release = false
1153 $gesture_press = nil
1158 #- handle reordering with drag and drop
1159 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1160 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1161 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1162 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1165 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1167 #- mouse gesture first (dnd disables button-release-event)
1168 if $gesture_press && $gesture_press[:filename] == filename
1169 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1170 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1171 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1172 rotate_and_cleanup.call(angle)
1173 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1175 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1176 msg 3, "gesture delete: click-drag right button to the bottom"
1178 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1183 ctxt.targets.each { |target|
1184 if target.name == 'reorder-elements'
1185 move_dnd = Proc.new { |from,to|
1188 autotable.move(from, to)
1189 save_undo(_("reorder"),
1190 Proc.new { |from, to|
1192 autotable.move(to - 1, from)
1194 autotable.move(to, from + 1)
1196 $notebook.set_page(1)
1198 autotable.move(from, to)
1199 $notebook.set_page(1)
1204 if $multiple_dnd.size == 0
1205 move_dnd.call(selection_data.data.to_i,
1206 autotable.get_current_number(vbox))
1208 UndoHandler.begin_batch
1209 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1211 #- need to update current position between each call
1212 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1213 autotable.get_current_number(vbox))
1215 UndoHandler.end_batch
1226 def create_auto_table
1228 $autotable = Gtk::AutoTable.new(5)
1230 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1231 thumbnails_vb = Gtk::VBox.new(false, 5)
1233 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1234 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1235 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1236 thumbnails_vb.add($autotable)
1238 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1239 $autotable_sw.add_with_viewport(thumbnails_vb)
1241 #- follows stuff for handling multiple elements selection
1242 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1244 update_selected = Proc.new {
1245 $autotable.current_order.each { |path|
1246 w = $name2widgets[path][:evtbox].window
1247 xm = w.position[0] + w.size[0]/2
1248 ym = w.position[1] + w.size[1]/2
1249 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1250 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1251 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1252 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1255 if $selected_elements[path] && ! $selected_elements[path][:keep]
1256 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))
1257 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1258 $selected_elements.delete(path)
1263 $autotable.signal_connect('realize') { |w,e|
1264 gc = Gdk::GC.new($autotable.window)
1265 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1266 gc.function = Gdk::GC::INVERT
1267 #- autoscroll handling for DND and multiple selections
1268 Gtk.timeout_add(100) {
1269 w, x, y, mask = $autotable.window.pointer
1270 if mask & Gdk::Window::BUTTON1_MASK != 0
1271 if y < $autotable_sw.vadjustment.value
1273 $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]])
1275 if $button1_pressed_autotable || press_x
1276 scroll_upper($autotable_sw, y)
1279 w, pos_x, pos_y = $autotable.window.pointer
1280 $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]])
1281 update_selected.call
1284 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1286 $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]])
1288 if $button1_pressed_autotable || press_x
1289 scroll_lower($autotable_sw, y)
1292 w, pos_x, pos_y = $autotable.window.pointer
1293 $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]])
1294 update_selected.call
1302 $autotable.signal_connect('button-press-event') { |w,e|
1304 if !$button1_pressed_autotable
1307 if e.state & Gdk::Window::SHIFT_MASK == 0
1308 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1309 $selected_elements = {}
1310 $statusbar.push(0, utf8(_("Nothing selected.")))
1312 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1314 set_mousecursor(Gdk::Cursor::TCROSS)
1318 $autotable.signal_connect('button-release-event') { |w,e|
1320 if $button1_pressed_autotable
1321 #- unselect all only now
1322 $multiple_dnd = $selected_elements.keys
1323 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1324 $selected_elements = {}
1325 $button1_pressed_autotable = false
1328 $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]])
1329 if $selected_elements.length > 0
1330 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1333 press_x = press_y = pos_x = pos_y = nil
1334 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1338 $autotable.signal_connect('motion-notify-event') { |w,e|
1341 $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]])
1345 $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]])
1346 update_selected.call
1352 def create_subalbums_page
1354 subalbums_hb = Gtk::HBox.new
1355 $subalbums_vb = Gtk::VBox.new(false, 5)
1356 subalbums_hb.pack_start($subalbums_vb, false, false)
1357 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1358 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1359 $subalbums_sw.add_with_viewport(subalbums_hb)
1362 def save_current_file
1366 ios = File.open($filename, "w")
1367 $xmldoc.write(ios, 0)
1372 def save_current_file_user
1373 save_tempfilename = $filename
1374 $filename = $orig_filename
1377 $generated_outofline = false
1378 $filename = save_tempfilename
1380 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1381 $todelete.each { |f|
1382 system("rm -f #{f}")
1386 def mark_document_as_dirty
1387 $xmldoc.elements.each('//dir') { |elem|
1388 elem.delete_attribute('already-generated')
1392 #- ret: true => ok false => cancel
1393 def ask_save_modifications(msg1, msg2, *options)
1395 options = options.size > 0 ? options[0] : {}
1397 if options[:disallow_cancel]
1398 dialog = Gtk::Dialog.new(msg1,
1400 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1401 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1402 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1404 dialog = Gtk::Dialog.new(msg1,
1406 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1407 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1408 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1409 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1411 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1412 dialog.vbox.add(Gtk::Label.new(msg2))
1413 dialog.window_position = Gtk::Window::POS_CENTER
1416 dialog.run { |response|
1418 if response == Gtk::Dialog::RESPONSE_YES
1419 save_current_file_user
1421 #- if we have generated an album but won't save modifications, we must remove
1422 #- already-generated markers in original file
1423 if $generated_outofline
1425 $xmldoc = REXML::Document.new File.new($orig_filename)
1426 mark_document_as_dirty
1427 ios = File.open($orig_filename, "w")
1428 $xmldoc.write(ios, 0)
1431 puts "exception: #{$!}"
1435 if response == Gtk::Dialog::RESPONSE_CANCEL
1438 $todelete = [] #- unconditionally clear the list of images/videos to delete
1444 def try_quit(*options)
1445 if ask_save_modifications(utf8(_("Save before quitting?")),
1446 utf8(_("Do you want to save your changes before quitting?")),
1452 def show_popup(parent, msg, *options)
1453 dialog = Gtk::Dialog.new
1454 dialog.title = utf8(_("Booh message"))
1455 lbl = Gtk::Label.new
1457 if options[0] && options[0][:centered]
1458 lbl.set_justify(Gtk::Justification::CENTER)
1460 if options[0] && options[0][:topwidget]
1461 dialog.vbox.add(options[0][:topwidget])
1463 dialog.vbox.add(lbl)
1464 if options[0] && options[0][:okcancel]
1465 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1467 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1469 dialog.set_default_size(200, 120)
1470 if options[0] && options[0][:pos_centered]
1471 dialog.window_position = Gtk::Window::POS_CENTER
1473 dialog.window_position = Gtk::Window::POS_MOUSE
1476 if options[0] && options[0][:linkurl]
1477 linkbut = Gtk::Button.new('')
1478 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1479 linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1480 linkbut.relief = Gtk::RELIEF_NONE
1481 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1482 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1483 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1488 if !options[0] || !options[0][:not_transient]
1489 dialog.transient_for = parent
1490 dialog.run { |response|
1492 if options[0] && options[0][:okcancel]
1493 return response == Gtk::Dialog::RESPONSE_OK
1497 dialog.signal_connect('response') { dialog.destroy }
1501 def backend_wait_message(parent, msg, infopipe_path, mode)
1503 w.set_transient_for(parent)
1506 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1507 vb.pack_start(Gtk::Label.new(msg), false, false)
1509 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1510 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1511 if mode != 'one dir scan'
1512 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1514 if mode == 'web-album'
1515 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1516 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1518 vb.pack_start(Gtk::HSeparator.new, false, false)
1520 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1521 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1522 vb.pack_end(bottom, false, false)
1524 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1525 refresh_thread = Thread.new {
1526 directories_counter = 0
1527 while line = infopipe.gets
1528 if line =~ /^directories: (\d+), sizes: (\d+)/
1529 directories = $1.to_f + 1
1531 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1532 elements = $3.to_f + 1
1533 if mode == 'web-album'
1537 gtk_thread_protect { pb1_1.fraction = 0 }
1538 if mode != 'one dir scan'
1539 newtext = utf8(full_src_dir_to_rel($1, $2))
1540 newtext = '/' if newtext == ''
1541 gtk_thread_protect { pb1_2.text = newtext }
1542 directories_counter += 1
1543 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1545 elsif line =~ /^processing element$/
1546 element_counter += 1
1547 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1548 elsif line =~ /^processing size$/
1549 element_counter += 1
1550 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1551 elsif line =~ /^finished processing sizes$/
1552 gtk_thread_protect { pb1_1.fraction = 1 }
1553 elsif line =~ /^creating index.html$/
1554 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1555 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1556 directories_counter = 0
1557 elsif line =~ /^index.html: (.+)\|(.+)/
1558 newtext = utf8(full_src_dir_to_rel($1, $2))
1559 newtext = '/' if newtext == ''
1560 gtk_thread_protect { pb2.text = newtext }
1561 directories_counter += 1
1562 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1568 w.signal_connect('delete-event') { w.destroy }
1569 w.signal_connect('destroy') {
1570 Thread.kill(refresh_thread)
1571 gtk_thread_abandon #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1574 system("rm -f #{infopipe_path}")
1577 w.window_position = Gtk::Window::POS_CENTER
1583 def call_backend(cmd, waitmsg, mode, params)
1584 pipe = Tempfile.new("boohpipe")
1586 system("mkfifo #{pipe.path}")
1587 cmd += " --info-pipe #{pipe.path}"
1588 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
1593 id, exitstatus = Process.waitpid2(pid)
1594 gtk_thread_protect { w8.destroy }
1596 if params[:successmsg]
1597 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
1599 if params[:closure_after]
1600 gtk_thread_protect(¶ms[:closure_after])
1602 elsif exitstatus == 15
1603 #- say nothing, user aborted
1605 if params[:failuremsg]
1606 gtk_thread_protect { show_popup($main_window, params[:failuremsg]) }
1613 button.signal_connect('clicked') {
1614 Process.kill('SIGTERM', pid)
1618 def save_changes(*forced)
1619 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1623 $xmldir.delete_attribute('already-generated')
1625 propagate_children = Proc.new { |xmldir|
1626 if xmldir.attributes['subdirs-caption']
1627 xmldir.delete_attribute('already-generated')
1629 xmldir.elements.each('dir') { |element|
1630 propagate_children.call(element)
1634 if $xmldir.child_byname_notattr('dir', 'deleted')
1635 new_title = $subalbums_title.buffer.text
1636 if new_title != $xmldir.attributes['subdirs-caption']
1637 parent = $xmldir.parent
1638 if parent.name == 'dir'
1639 parent.delete_attribute('already-generated')
1641 propagate_children.call($xmldir)
1643 $xmldir.add_attribute('subdirs-caption', new_title)
1644 $xmldir.elements.each('dir') { |element|
1645 if !element.attributes['deleted']
1646 path = element.attributes['path']
1647 newtext = $subalbums_edits[path][:editzone].buffer.text
1648 if element.attributes['subdirs-caption']
1649 if element.attributes['subdirs-caption'] != newtext
1650 propagate_children.call(element)
1652 element.add_attribute('subdirs-caption', newtext)
1653 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1655 if element.attributes['thumbnails-caption'] != newtext
1656 element.delete_attribute('already-generated')
1658 element.add_attribute('thumbnails-caption', newtext)
1659 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1665 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
1666 if $xmldir.attributes['thumbnails-caption']
1667 path = $xmldir.attributes['path']
1668 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1670 elsif $xmldir.attributes['thumbnails-caption']
1671 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
1674 #- remove and reinsert elements to reflect new ordering
1677 $xmldir.elements.each { |element|
1678 if element.name == 'image' || element.name == 'video'
1679 saves[element.attributes['filename']] = element.remove
1683 $autotable.current_order.each { |path|
1684 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1685 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1688 saves.each_key { |path|
1689 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1690 chld.add_attribute('deleted', 'true')
1694 def remove_all_captions
1697 $autotable.current_order.each { |path|
1698 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
1699 $name2widgets[File.basename(path)][:textview].buffer.text = ''
1701 save_undo(_("remove all captions"),
1703 texts.each_key { |key|
1704 $name2widgets[key][:textview].buffer.text = texts[key]
1706 $notebook.set_page(1)
1708 texts.each_key { |key|
1709 $name2widgets[key][:textview].buffer.text = ''
1711 $notebook.set_page(1)
1717 $selected_elements.each_key { |path|
1718 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1724 $selected_elements = {}
1728 $undo_tb.sensitive = $undo_mb.sensitive = false
1729 $redo_tb.sensitive = $redo_mb.sensitive = false
1735 $subalbums_vb.children.each { |chld|
1736 $subalbums_vb.remove(chld)
1738 $subalbums = Gtk::Table.new(0, 0, true)
1739 current_y_sub_albums = 0
1741 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
1742 $subalbums_edits = {}
1743 subalbums_counter = 0
1744 subalbums_edits_bypos = {}
1746 add_subalbum = Proc.new { |xmldir, counter|
1747 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
1748 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
1749 if xmldir == $xmldir
1750 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
1751 caption = xmldir.attributes['thumbnails-caption']
1752 captionfile, dummy = find_subalbum_caption_info(xmldir)
1753 infotype = 'thumbnails'
1755 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
1756 captionfile, caption = find_subalbum_caption_info(xmldir)
1757 infotype = find_subalbum_info_type(xmldir)
1759 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
1760 hbox = Gtk::HBox.new
1761 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
1763 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
1766 my_gen_real_thumbnail = proc {
1767 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
1770 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
1771 f.add(img = Gtk::Image.new)
1772 my_gen_real_thumbnail.call
1774 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
1776 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
1777 $subalbums.attach(hbox,
1778 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1780 frame, textview = create_editzone($subalbums_sw, 0, img)
1781 textview.buffer.text = caption
1782 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
1783 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1785 change_image = Proc.new {
1786 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
1788 Gtk::FileChooser::ACTION_OPEN,
1790 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1791 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
1792 fc.transient_for = $main_window
1793 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))
1794 f.add(preview_img = Gtk::Image.new)
1796 fc.signal_connect('update-preview') { |w|
1798 if fc.preview_filename
1799 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
1800 fc.preview_widget_active = true
1802 rescue Gdk::PixbufError
1803 fc.preview_widget_active = false
1806 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1808 old_file = captionfile
1809 old_rotate = xmldir.attributes["#{infotype}-rotate"]
1810 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
1811 old_enhance = xmldir.attributes["#{infotype}-enhance"]
1812 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
1814 new_file = fc.filename
1815 msg 3, "new captionfile is: #{fc.filename}"
1816 perform_changefile = Proc.new {
1817 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
1818 $modified_pixbufs.delete(thumbnail_file)
1819 xmldir.delete_attribute("#{infotype}-rotate")
1820 xmldir.delete_attribute("#{infotype}-color-swap")
1821 xmldir.delete_attribute("#{infotype}-enhance")
1822 xmldir.delete_attribute("#{infotype}-frame-offset")
1823 my_gen_real_thumbnail.call
1825 perform_changefile.call
1827 save_undo(_("change caption file for sub-album"),
1829 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
1830 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
1831 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
1832 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
1833 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
1834 my_gen_real_thumbnail.call
1835 $notebook.set_page(0)
1837 perform_changefile.call
1838 $notebook.set_page(0)
1845 rotate_and_cleanup = Proc.new { |angle|
1846 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
1847 system("rm -f '#{thumbnail_file}'")
1850 move = Proc.new { |direction|
1853 save_changes('forced')
1854 if direction == 'up'
1855 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
1856 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
1857 subalbums_edits_bypos[oldpos - 1][:position] += 1
1859 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
1860 $subalbums_edits[xmldir.attributes['path']][:position] += 1
1861 subalbums_edits_bypos[oldpos + 1][:position] -= 1
1865 $xmldir.elements.each('dir') { |element|
1866 if (!element.attributes['deleted'])
1867 elems << [ element.attributes['path'], element.remove ]
1870 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
1871 each { |e| $xmldir.add_element(e[1]) }
1872 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
1873 $xmldir.elements.each('descendant::dir') { |elem|
1874 elem.delete_attribute('already-generated')
1879 color_swap_and_cleanup = Proc.new {
1880 perform_color_swap_and_cleanup = Proc.new {
1881 color_swap(xmldir, "#{infotype}-")
1882 my_gen_real_thumbnail.call
1884 perform_color_swap_and_cleanup.call
1886 save_undo(_("color swap"),
1888 perform_color_swap_and_cleanup.call
1889 $notebook.set_page(0)
1891 perform_color_swap_and_cleanup.call
1892 $notebook.set_page(0)
1897 change_frame_offset_and_cleanup = Proc.new {
1898 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
1899 perform_change_frame_offset_and_cleanup = Proc.new { |val|
1900 change_frame_offset(xmldir, "#{infotype}-", val)
1901 my_gen_real_thumbnail.call
1903 perform_change_frame_offset_and_cleanup.call(values[:new])
1905 save_undo(_("specify frame offset"),
1907 perform_change_frame_offset_and_cleanup.call(values[:old])
1908 $notebook.set_page(0)
1910 perform_change_frame_offset_and_cleanup.call(values[:new])
1911 $notebook.set_page(0)
1917 whitebalance_and_cleanup = Proc.new {
1918 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
1919 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
1920 perform_change_whitebalance_and_cleanup = Proc.new { |val|
1921 change_whitebalance(xmldir, "#{infotype}-", val)
1922 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
1923 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
1924 system("rm -f '#{thumbnail_file}'")
1926 perform_change_whitebalance_and_cleanup.call(values[:new])
1928 save_undo(_("fix white balance"),
1930 perform_change_whitebalance_and_cleanup.call(values[:old])
1931 $notebook.set_page(0)
1933 perform_change_whitebalance_and_cleanup.call(values[:new])
1934 $notebook.set_page(0)
1940 enhance_and_cleanup = Proc.new {
1941 perform_enhance_and_cleanup = Proc.new {
1942 enhance(xmldir, "#{infotype}-")
1943 my_gen_real_thumbnail.call
1946 perform_enhance_and_cleanup.call
1948 save_undo(_("enhance"),
1950 perform_enhance_and_cleanup.call
1951 $notebook.set_page(0)
1953 perform_enhance_and_cleanup.call
1954 $notebook.set_page(0)
1959 evtbox.signal_connect('button-press-event') { |w, event|
1960 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1962 rotate_and_cleanup.call(90)
1964 rotate_and_cleanup.call(-90)
1965 elsif $enhance.active?
1966 enhance_and_cleanup.call
1969 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1970 popup_thumbnail_menu(event, ['change_image'], entry2type(captionfile), xmldir, "#{infotype}-",
1971 { :forbid_left => true, :forbid_right => true,
1972 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter },
1973 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
1974 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
1976 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1981 evtbox.signal_connect('button-press-event') { |w, event|
1982 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
1986 evtbox.signal_connect('button-release-event') { |w, event|
1987 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
1988 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
1989 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
1990 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
1991 msg 3, "gesture rotate: #{angle}"
1992 rotate_and_cleanup.call(angle)
1995 $gesture_press = nil
1998 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
1999 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2000 current_y_sub_albums += 1
2003 if $xmldir.child_byname_notattr('dir', 'deleted')
2005 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2006 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2007 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2008 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2009 #- this album image/caption
2010 if $xmldir.attributes['thumbnails-caption']
2011 add_subalbum.call($xmldir, 0)
2014 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2015 $xmldir.elements.each { |element|
2016 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2017 #- element (image or video) of this album
2018 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2019 msg 3, "dest_img: #{dest_img}"
2020 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
2021 total[element.name] += 1
2023 if element.name == 'dir' && !element.attributes['deleted']
2024 #- sub-album image/caption
2025 add_subalbum.call(element, subalbums_counter += 1)
2026 total[element.name] += 1
2029 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2030 total['image'], total['video'], total['dir'] ]))
2031 $subalbums_vb.add($subalbums)
2032 $subalbums_vb.show_all
2034 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2035 $notebook.get_tab_label($autotable_sw).sensitive = false
2036 $notebook.set_page(0)
2037 $thumbnails_title.buffer.text = ''
2039 $notebook.get_tab_label($autotable_sw).sensitive = true
2040 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2043 if !$xmldir.child_byname_notattr('dir', 'deleted')
2044 $notebook.get_tab_label($subalbums_sw).sensitive = false
2045 $notebook.set_page(1)
2047 $notebook.get_tab_label($subalbums_sw).sensitive = true
2051 def pixbuf_or_nil(filename)
2053 return Gdk::Pixbuf.new(filename)
2059 def theme_choose(current)
2060 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2062 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2063 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2064 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2066 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2067 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2068 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2069 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2070 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2071 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2072 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2073 treeview.signal_connect('button-press-event') { |w, event|
2074 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2075 dialog.response(Gtk::Dialog::RESPONSE_OK)
2079 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2081 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2084 iter[0] = File.basename(dir)
2085 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2086 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2087 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2088 if File.basename(dir) == current
2089 treeview.selection.select_iter(iter)
2093 dialog.set_default_size(700, 400)
2094 dialog.vbox.show_all
2095 dialog.run { |response|
2096 iter = treeview.selection.selected
2098 if response == Gtk::Dialog::RESPONSE_OK && iter
2099 return model.get_value(iter, 0)
2105 def populate_subalbums_treeview
2108 $subalbums_vb.children.each { |chld|
2109 $subalbums_vb.remove(chld)
2112 source = $xmldoc.root.attributes['source']
2113 msg 3, "source: #{source}"
2115 xmldir = $xmldoc.elements['//dir']
2116 if !xmldir || xmldir.attributes['path'] != source
2117 msg 1, _("Corrupted booh file...")
2121 append_dir_elem = Proc.new { |parent_iter, xmldir|
2122 child_iter = $albums_ts.append(parent_iter)
2123 child_iter[0] = File.basename(xmldir.attributes['path'])
2124 child_iter[1] = xmldir.attributes['path']
2125 msg 3, "puttin location: #{xmldir.attributes['path']}"
2126 xmldir.elements.each('dir') { |elem|
2127 if !elem.attributes['deleted']
2128 append_dir_elem.call(child_iter, elem)
2132 append_dir_elem.call(nil, xmldir)
2134 $albums_tv.expand_all
2135 $albums_tv.selection.select_iter($albums_ts.iter_first)
2138 def open_file(filename)
2142 $current_path = nil #- invalidate
2143 $modified_pixbufs = {}
2146 $subalbums_vb.children.each { |chld|
2147 $subalbums_vb.remove(chld)
2150 if !File.exists?(filename)
2151 return utf8(_("File not found."))
2155 $xmldoc = REXML::Document.new File.new(filename)
2160 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2161 if entry2type(filename).nil?
2162 return utf8(_("Not a booh file!"))
2164 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."))
2168 if !source = $xmldoc.root.attributes['source']
2169 return utf8(_("Corrupted booh file..."))
2172 if !dest = $xmldoc.root.attributes['destination']
2173 return utf8(_("Corrupted booh file..."))
2176 if !theme = $xmldoc.root.attributes['theme']
2177 return utf8(_("Corrupted booh file..."))
2180 if $xmldoc.root.attributes['version'] != $VERSION
2181 msg 2, _("File's version %s, booh version now #{$VERSION}, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2182 mark_document_as_dirty
2183 $xmldoc.root.add_attribute('version', $VERSION)
2186 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2187 optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2188 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2190 $filename = filename
2191 select_theme(theme, limit_sizes, optimizefor32, nperrow)
2192 $default_size['thumbnails'] =~ /(.*)x(.*)/
2193 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2194 $albums_thumbnail_size =~ /(.*)x(.*)/
2195 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2197 populate_subalbums_treeview
2199 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge.sensitive = $generate.sensitive = $properties.sensitive = $remove_all_captions.sensitive = true
2203 def open_file_user(filename)
2204 result = open_file(filename)
2206 $config['last-opens'] ||= []
2207 if $config['last-opens'][-1] != utf8(filename)
2208 $config['last-opens'] << utf8(filename)
2210 $orig_filename = $filename
2211 tmp = Tempfile.new("boohtemp")
2214 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2216 $tempfiles << $filename << "#{$filename}.backup"
2218 $orig_filename = nil
2224 if !ask_save_modifications(utf8(_("Save this album?")),
2225 utf8(_("Do you want to save the changes to this album?")),
2226 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2229 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2231 Gtk::FileChooser::ACTION_OPEN,
2233 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2234 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2235 fc.set_current_folder(File.expand_path("~/.booh"))
2236 fc.transient_for = $main_window
2239 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2240 push_mousecursor_wait(fc)
2241 msg = open_file_user(fc.filename)
2257 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2262 def additional_booh_options
2265 options += "--mproc #{$config['mproc'].to_i} "
2267 if $config['emptycomments']
2268 options += "--empty-comments "
2274 if !ask_save_modifications(utf8(_("Save this album?")),
2275 utf8(_("Do you want to save the changes to this album?")),
2276 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2279 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2281 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2282 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2283 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2285 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2286 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2287 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2288 tbl.attach(src = Gtk::Entry.new,
2289 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2290 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2291 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2292 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2293 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2294 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2295 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2296 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2297 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2298 tbl.attach(dest = Gtk::Entry.new,
2299 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2300 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2301 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2302 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2303 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2304 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2305 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2306 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2307 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2309 tooltips = Gtk::Tooltips.new
2310 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2311 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2312 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2313 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2314 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2315 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2316 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)
2317 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2318 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2320 src_nb_calculated_for = ''
2322 process_src_nb = Proc.new {
2323 if src.text != src_nb_calculated_for
2324 src_nb_calculated_for = src.text
2326 Thread.kill(src_nb_thread)
2329 if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2330 if File.readable?(from_utf8(src_nb_calculated_for))
2331 src_nb_thread = Thread.new {
2332 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2333 total = { 'image' => 0, 'video' => 0, nil => 0 }
2334 `find '#{from_utf8(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2335 if File.basename(dir) =~ /^\./
2339 Dir.entries(dir.chomp).each { |file|
2340 total[entry2type(file)] += 1
2342 rescue Errno::EACCES, Errno::ENOENT
2346 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2350 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2353 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2358 timeout_src_nb = Gtk.timeout_add(100) {
2362 src_browse.signal_connect('clicked') {
2363 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2365 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2367 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2368 fc.transient_for = $main_window
2369 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2370 src.text = utf8(fc.filename)
2372 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2377 dest_browse.signal_connect('clicked') {
2378 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2380 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2382 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2383 fc.transient_for = $main_window
2384 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2385 dest.text = utf8(fc.filename)
2390 conf_browse.signal_connect('clicked') {
2391 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2393 Gtk::FileChooser::ACTION_SAVE,
2395 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2396 fc.transient_for = $main_window
2397 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2398 fc.set_current_folder(File.expand_path("~/.booh"))
2399 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2400 conf.text = utf8(fc.filename)
2407 recreate_theme_config = proc {
2408 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2410 select_theme(theme_button.label, 'all', optimize432.active?, nil)
2411 $images_size.each { |s|
2412 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2416 tooltips.set_tip(cb, utf8(s['description']), nil)
2417 theme_sizes << { :widget => cb, :value => s['name'] }
2419 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2420 tooltips = Gtk::Tooltips.new
2421 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2422 theme_sizes << { :widget => cb, :value => 'original' }
2425 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2428 $allowed_N_values.each { |n|
2430 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2432 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2437 nperrows << { :widget => rb, :value => n }
2439 nperrowradios.show_all
2441 recreate_theme_config.call
2443 theme_button.signal_connect('clicked') {
2444 if newtheme = theme_choose(theme_button.label)
2445 theme_button.label = newtheme
2446 recreate_theme_config.call
2450 dialog.vbox.add(frame1)
2451 dialog.vbox.add(frame2)
2452 dialog.window_position = Gtk::Window::POS_MOUSE
2458 dialog.run { |response|
2459 if response == Gtk::Dialog::RESPONSE_OK
2460 srcdir = from_utf8(src.text)
2461 destdir = from_utf8(dest.text)
2462 if !File.directory?(srcdir)
2463 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2465 elsif conf.text == ''
2466 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2468 elsif File.directory?(from_utf8(conf.text))
2469 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
2471 elsif destdir != make_dest_filename(destdir)
2472 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2474 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
2475 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2477 elsif File.exists?(destdir) && !File.directory?(destdir)
2478 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2480 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2481 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2483 system("mkdir '#{destdir}'")
2484 if !File.directory?(destdir)
2485 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2496 srcdir = from_utf8(src.text)
2497 destdir = from_utf8(dest.text)
2498 configskel = File.expand_path(from_utf8(conf.text))
2499 theme = theme_button.label
2500 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2501 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2502 opt432 = optimize432.active?
2504 Thread.kill(src_nb_thread)
2505 gtk_thread_abandon #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2508 Gtk.timeout_remove(timeout_src_nb)
2511 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2512 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2513 "#{opt432 ? '--optimize-for-32' : ''} #{additional_booh_options}",
2514 utf8(_("Please wait while scanning source directory...")),
2516 { :closure_after => proc { open_file_user(configskel) } })
2521 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2523 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2524 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2525 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2527 source = $xmldoc.root.attributes['source']
2528 dest = $xmldoc.root.attributes['destination']
2529 theme = $xmldoc.root.attributes['theme']
2530 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2531 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2532 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2534 limit_sizes = limit_sizes.split(/,/)
2537 tooltips = Gtk::Tooltips.new
2538 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2539 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2540 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2541 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2542 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2543 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2544 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2545 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>')),
2546 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2547 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2548 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2549 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>')),
2550 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2552 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2553 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2554 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2555 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2556 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2557 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2558 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)
2559 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2560 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2564 recreate_theme_config = proc {
2565 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2567 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
2569 $images_size.each { |s|
2570 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2572 if limit_sizes.include?(s['name'])
2580 tooltips.set_tip(cb, utf8(s['description']), nil)
2581 theme_sizes << { :widget => cb, :value => s['name'] }
2583 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2584 tooltips = Gtk::Tooltips.new
2585 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2586 if limit_sizes && limit_sizes.include?('original')
2589 theme_sizes << { :widget => cb, :value => 'original' }
2592 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2595 $allowed_N_values.each { |n|
2597 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2599 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2601 nperrowradios.add(Gtk::Label.new(' '))
2602 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
2605 nperrows << { :widget => rb, :value => n.to_s }
2607 nperrowradios.show_all
2609 recreate_theme_config.call
2611 theme_button.signal_connect('clicked') {
2612 if newtheme = theme_choose(theme_button.label)
2615 theme_button.label = newtheme
2616 recreate_theme_config.call
2620 dialog.vbox.add(frame1)
2621 dialog.vbox.add(frame2)
2622 dialog.window_position = Gtk::Window::POS_MOUSE
2628 dialog.run { |response|
2629 if response == Gtk::Dialog::RESPONSE_OK
2630 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2631 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2640 save_theme = theme_button.label
2641 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
2642 save_opt432 = optimize432.active?
2643 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2646 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow)
2647 mark_document_as_dirty
2649 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
2650 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
2651 "#{save_opt432 ? '--optimize-for-32' : ''} #{additional_booh_options}",
2652 utf8(_("Please wait while scanning source directory...")),
2654 { :closure_after => proc {
2655 open_file($filename)
2664 sel = $albums_tv.selection.selected_rows
2666 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2667 "--verbose-level #{$verbose_level} #{additional_booh_options}",
2668 utf8(_("Please wait while scanning source directory...")),
2670 { :closure_after => proc {
2671 open_file($filename)
2672 $albums_tv.selection.select_path(sel[0])
2680 theme = $xmldoc.root.attributes['theme']
2681 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2683 limit_sizes = "--sizes #{limit_sizes}"
2685 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
2686 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
2687 utf8(_("Please wait while scanning source directory...")),
2689 { :closure_after => proc {
2690 open_file($filename)
2696 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
2698 Gtk::FileChooser::ACTION_SAVE,
2700 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2701 fc.transient_for = $main_window
2702 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2703 fc.set_current_folder(File.expand_path("~/.booh"))
2704 fc.filename = $orig_filename
2705 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2706 $orig_filename = fc.filename
2707 save_current_file_user
2713 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
2715 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2716 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2717 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2719 dialog.vbox.add(notebook = Gtk::Notebook.new)
2720 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
2721 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
2722 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2723 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
2724 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2725 tooltips = Gtk::Tooltips.new
2726 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
2727 for example: /usr/bin/mplayer %f")), nil)
2728 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
2729 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2730 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
2731 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2732 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
2733 for example: /usr/bin/mozilla-firefox %f")), nil)
2734 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
2735 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2736 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)),
2737 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2738 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)
2739 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
2740 0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2741 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)
2742 tbl.attach(emptycomments_check = Gtk::CheckButton.new(utf8(_("Use empty comments for new albums"))),
2743 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
2744 tooltips.set_tip(emptycomments_check, utf8(_("Normally, filenames are used as comments for new albums. Check this if you prefer empty comments.")), nil)
2745 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
2746 0, 2, 5, 6, Gtk::FILL, Gtk::SHRINK, 2, 2)
2747 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)
2748 smp_check.signal_connect('toggled') {
2749 if smp_check.active?
2750 smp_hbox.sensitive = true
2752 smp_hbox.sensitive = false
2756 smp_check.active = true
2757 smp_spin.value = $config['mproc'].to_i
2759 nogestures_check.active = $config['nogestures']
2760 emptycomments_check.active = $config['emptycomments']
2761 deleteondisk_check.active = $config['deleteondisk']
2763 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
2764 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
2765 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2766 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
2767 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2769 dialog.vbox.show_all
2770 dialog.run { |response|
2771 if response == Gtk::Dialog::RESPONSE_OK
2772 $config['video-viewer'] = video_viewer_entry.text
2773 $config['browser'] = browser_entry.text
2774 if smp_check.active?
2775 $config['mproc'] = smp_spin.value.to_i
2777 $config.delete('mproc')
2779 $config['nogestures'] = nogestures_check.active?
2780 $config['emptycomments'] = emptycomments_check.active?
2781 $config['deleteondisk'] = deleteondisk_check.active?
2783 $config['convert-enhance'] = enhance_entry.text
2790 if $undo_tb.sensitive?
2791 $redo_tb.sensitive = $redo_mb.sensitive = true
2792 if not more_undoes = UndoHandler.undo($statusbar)
2793 $undo_tb.sensitive = $undo_mb.sensitive = false
2799 if $redo_tb.sensitive?
2800 $undo_tb.sensitive = $undo_mb.sensitive = true
2801 if not more_redoes = UndoHandler.redo($statusbar)
2802 $redo_tb.sensitive = $redo_mb.sensitive = false
2807 def show_one_click_explanation(intro)
2808 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
2810 %s When such a tool is activated
2811 (<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
2812 on a thumbnail will immediately apply the desired action.
2814 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
2820 GNU GENERAL PUBLIC LICENSE
2821 Version 2, June 1991
2823 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
2824 675 Mass Ave, Cambridge, MA 02139, USA
2825 Everyone is permitted to copy and distribute verbatim copies
2826 of this license document, but changing it is not allowed.
2830 The licenses for most software are designed to take away your
2831 freedom to share and change it. By contrast, the GNU General Public
2832 License is intended to guarantee your freedom to share and change free
2833 software--to make sure the software is free for all its users. This
2834 General Public License applies to most of the Free Software
2835 Foundation's software and to any other program whose authors commit to
2836 using it. (Some other Free Software Foundation software is covered by
2837 the GNU Library General Public License instead.) You can apply it to
2840 When we speak of free software, we are referring to freedom, not
2841 price. Our General Public Licenses are designed to make sure that you
2842 have the freedom to distribute copies of free software (and charge for
2843 this service if you wish), that you receive source code or can get it
2844 if you want it, that you can change the software or use pieces of it
2845 in new free programs; and that you know you can do these things.
2847 To protect your rights, we need to make restrictions that forbid
2848 anyone to deny you these rights or to ask you to surrender the rights.
2849 These restrictions translate to certain responsibilities for you if you
2850 distribute copies of the software, or if you modify it.
2852 For example, if you distribute copies of such a program, whether
2853 gratis or for a fee, you must give the recipients all the rights that
2854 you have. You must make sure that they, too, receive or can get the
2855 source code. And you must show them these terms so they know their
2858 We protect your rights with two steps: (1) copyright the software, and
2859 (2) offer you this license which gives you legal permission to copy,
2860 distribute and/or modify the software.
2862 Also, for each author's protection and ours, we want to make certain
2863 that everyone understands that there is no warranty for this free
2864 software. If the software is modified by someone else and passed on, we
2865 want its recipients to know that what they have is not the original, so
2866 that any problems introduced by others will not reflect on the original
2867 authors' reputations.
2869 Finally, any free program is threatened constantly by software
2870 patents. We wish to avoid the danger that redistributors of a free
2871 program will individually obtain patent licenses, in effect making the
2872 program proprietary. To prevent this, we have made it clear that any
2873 patent must be licensed for everyone's free use or not licensed at all.
2875 The precise terms and conditions for copying, distribution and
2876 modification follow.
2879 GNU GENERAL PUBLIC LICENSE
2880 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
2882 0. This License applies to any program or other work which contains
2883 a notice placed by the copyright holder saying it may be distributed
2884 under the terms of this General Public License. The "Program", below,
2885 refers to any such program or work, and a "work based on the Program"
2886 means either the Program or any derivative work under copyright law:
2887 that is to say, a work containing the Program or a portion of it,
2888 either verbatim or with modifications and/or translated into another
2889 language. (Hereinafter, translation is included without limitation in
2890 the term "modification".) Each licensee is addressed as "you".
2892 Activities other than copying, distribution and modification are not
2893 covered by this License; they are outside its scope. The act of
2894 running the Program is not restricted, and the output from the Program
2895 is covered only if its contents constitute a work based on the
2896 Program (independent of having been made by running the Program).
2897 Whether that is true depends on what the Program does.
2899 1. You may copy and distribute verbatim copies of the Program's
2900 source code as you receive it, in any medium, provided that you
2901 conspicuously and appropriately publish on each copy an appropriate
2902 copyright notice and disclaimer of warranty; keep intact all the
2903 notices that refer to this License and to the absence of any warranty;
2904 and give any other recipients of the Program a copy of this License
2905 along with the Program.
2907 You may charge a fee for the physical act of transferring a copy, and
2908 you may at your option offer warranty protection in exchange for a fee.
2910 2. You may modify your copy or copies of the Program or any portion
2911 of it, thus forming a work based on the Program, and copy and
2912 distribute such modifications or work under the terms of Section 1
2913 above, provided that you also meet all of these conditions:
2915 a) You must cause the modified files to carry prominent notices
2916 stating that you changed the files and the date of any change.
2918 b) You must cause any work that you distribute or publish, that in
2919 whole or in part contains or is derived from the Program or any
2920 part thereof, to be licensed as a whole at no charge to all third
2921 parties under the terms of this License.
2923 c) If the modified program normally reads commands interactively
2924 when run, you must cause it, when started running for such
2925 interactive use in the most ordinary way, to print or display an
2926 announcement including an appropriate copyright notice and a
2927 notice that there is no warranty (or else, saying that you provide
2928 a warranty) and that users may redistribute the program under
2929 these conditions, and telling the user how to view a copy of this
2930 License. (Exception: if the Program itself is interactive but
2931 does not normally print such an announcement, your work based on
2932 the Program is not required to print an announcement.)
2935 These requirements apply to the modified work as a whole. If
2936 identifiable sections of that work are not derived from the Program,
2937 and can be reasonably considered independent and separate works in
2938 themselves, then this License, and its terms, do not apply to those
2939 sections when you distribute them as separate works. But when you
2940 distribute the same sections as part of a whole which is a work based
2941 on the Program, the distribution of the whole must be on the terms of
2942 this License, whose permissions for other licensees extend to the
2943 entire whole, and thus to each and every part regardless of who wrote it.
2945 Thus, it is not the intent of this section to claim rights or contest
2946 your rights to work written entirely by you; rather, the intent is to
2947 exercise the right to control the distribution of derivative or
2948 collective works based on the Program.
2950 In addition, mere aggregation of another work not based on the Program
2951 with the Program (or with a work based on the Program) on a volume of
2952 a storage or distribution medium does not bring the other work under
2953 the scope of this License.
2955 3. You may copy and distribute the Program (or a work based on it,
2956 under Section 2) in object code or executable form under the terms of
2957 Sections 1 and 2 above provided that you also do one of the following:
2959 a) Accompany it with the complete corresponding machine-readable
2960 source code, which must be distributed under the terms of Sections
2961 1 and 2 above on a medium customarily used for software interchange; or,
2963 b) Accompany it with a written offer, valid for at least three
2964 years, to give any third party, for a charge no more than your
2965 cost of physically performing source distribution, a complete
2966 machine-readable copy of the corresponding source code, to be
2967 distributed under the terms of Sections 1 and 2 above on a medium
2968 customarily used for software interchange; or,
2970 c) Accompany it with the information you received as to the offer
2971 to distribute corresponding source code. (This alternative is
2972 allowed only for noncommercial distribution and only if you
2973 received the program in object code or executable form with such
2974 an offer, in accord with Subsection b above.)
2976 The source code for a work means the preferred form of the work for
2977 making modifications to it. For an executable work, complete source
2978 code means all the source code for all modules it contains, plus any
2979 associated interface definition files, plus the scripts used to
2980 control compilation and installation of the executable. However, as a
2981 special exception, the source code distributed need not include
2982 anything that is normally distributed (in either source or binary
2983 form) with the major components (compiler, kernel, and so on) of the
2984 operating system on which the executable runs, unless that component
2985 itself accompanies the executable.
2987 If distribution of executable or object code is made by offering
2988 access to copy from a designated place, then offering equivalent
2989 access to copy the source code from the same place counts as
2990 distribution of the source code, even though third parties are not
2991 compelled to copy the source along with the object code.
2994 4. You may not copy, modify, sublicense, or distribute the Program
2995 except as expressly provided under this License. Any attempt
2996 otherwise to copy, modify, sublicense or distribute the Program is
2997 void, and will automatically terminate your rights under this License.
2998 However, parties who have received copies, or rights, from you under
2999 this License will not have their licenses terminated so long as such
3000 parties remain in full compliance.
3002 5. You are not required to accept this License, since you have not
3003 signed it. However, nothing else grants you permission to modify or
3004 distribute the Program or its derivative works. These actions are
3005 prohibited by law if you do not accept this License. Therefore, by
3006 modifying or distributing the Program (or any work based on the
3007 Program), you indicate your acceptance of this License to do so, and
3008 all its terms and conditions for copying, distributing or modifying
3009 the Program or works based on it.
3011 6. Each time you redistribute the Program (or any work based on the
3012 Program), the recipient automatically receives a license from the
3013 original licensor to copy, distribute or modify the Program subject to
3014 these terms and conditions. You may not impose any further
3015 restrictions on the recipients' exercise of the rights granted herein.
3016 You are not responsible for enforcing compliance by third parties to
3019 7. If, as a consequence of a court judgment or allegation of patent
3020 infringement or for any other reason (not limited to patent issues),
3021 conditions are imposed on you (whether by court order, agreement or
3022 otherwise) that contradict the conditions of this License, they do not
3023 excuse you from the conditions of this License. If you cannot
3024 distribute so as to satisfy simultaneously your obligations under this
3025 License and any other pertinent obligations, then as a consequence you
3026 may not distribute the Program at all. For example, if a patent
3027 license would not permit royalty-free redistribution of the Program by
3028 all those who receive copies directly or indirectly through you, then
3029 the only way you could satisfy both it and this License would be to
3030 refrain entirely from distribution of the Program.
3032 If any portion of this section is held invalid or unenforceable under
3033 any particular circumstance, the balance of the section is intended to
3034 apply and the section as a whole is intended to apply in other
3037 It is not the purpose of this section to induce you to infringe any
3038 patents or other property right claims or to contest validity of any
3039 such claims; this section has the sole purpose of protecting the
3040 integrity of the free software distribution system, which is
3041 implemented by public license practices. Many people have made
3042 generous contributions to the wide range of software distributed
3043 through that system in reliance on consistent application of that
3044 system; it is up to the author/donor to decide if he or she is willing
3045 to distribute software through any other system and a licensee cannot
3048 This section is intended to make thoroughly clear what is believed to
3049 be a consequence of the rest of this License.
3052 8. If the distribution and/or use of the Program is restricted in
3053 certain countries either by patents or by copyrighted interfaces, the
3054 original copyright holder who places the Program under this License
3055 may add an explicit geographical distribution limitation excluding
3056 those countries, so that distribution is permitted only in or among
3057 countries not thus excluded. In such case, this License incorporates
3058 the limitation as if written in the body of this License.
3060 9. The Free Software Foundation may publish revised and/or new versions
3061 of the General Public License from time to time. Such new versions will
3062 be similar in spirit to the present version, but may differ in detail to
3063 address new problems or concerns.
3065 Each version is given a distinguishing version number. If the Program
3066 specifies a version number of this License which applies to it and "any
3067 later version", you have the option of following the terms and conditions
3068 either of that version or of any later version published by the Free
3069 Software Foundation. If the Program does not specify a version number of
3070 this License, you may choose any version ever published by the Free Software
3073 10. If you wish to incorporate parts of the Program into other free
3074 programs whose distribution conditions are different, write to the author
3075 to ask for permission. For software which is copyrighted by the Free
3076 Software Foundation, write to the Free Software Foundation; we sometimes
3077 make exceptions for this. Our decision will be guided by the two goals
3078 of preserving the free status of all derivatives of our free software and
3079 of promoting the sharing and reuse of software generally.
3083 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
3084 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
3085 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
3086 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
3087 OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
3088 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
3089 TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
3090 PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
3091 REPAIR OR CORRECTION.
3093 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
3094 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
3095 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
3096 INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
3097 OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
3098 TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
3099 YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
3100 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
3101 POSSIBILITY OF SUCH DAMAGES.
3103 END OF TERMS AND CONDITIONS
3106 Appendix: How to Apply These Terms to Your New Programs
3108 If you develop a new program, and you want it to be of the greatest
3109 possible use to the public, the best way to achieve this is to make it
3110 free software which everyone can redistribute and change under these terms.
3112 To do so, attach the following notices to the program. It is safest
3113 to attach them to the start of each source file to most effectively
3114 convey the exclusion of warranty; and each file should have at least
3115 the "copyright" line and a pointer to where the full notice is found.
3117 <one line to give the program's name and a brief idea of what it does.>
3118 Copyright (C) 19yy <name of author>
3120 This program is free software; you can redistribute it and/or modify
3121 it under the terms of the GNU General Public License as published by
3122 the Free Software Foundation; either version 2 of the License, or
3123 (at your option) any later version.
3125 This program is distributed in the hope that it will be useful,
3126 but WITHOUT ANY WARRANTY; without even the implied warranty of
3127 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3128 GNU General Public License for more details.
3130 You should have received a copy of the GNU General Public License
3131 along with this program; if not, write to the Free Software
3132 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
3134 Also add information on how to contact you by electronic and paper mail.
3136 If the program is interactive, make it output a short notice like this
3137 when it starts in an interactive mode:
3139 Gnomovision version 69, Copyright (C) 19yy name of author
3140 Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
3141 This is free software, and you are welcome to redistribute it
3142 under certain conditions; type `show c' for details.
3144 The hypothetical commands `show w' and `show c' should show the appropriate
3145 parts of the General Public License. Of course, the commands you use may
3146 be called something other than `show w' and `show c'; they could even be
3147 mouse-clicks or menu items--whatever suits your program.
3149 You should also get your employer (if you work as a programmer) or your
3150 school, if any, to sign a "copyright disclaimer" for the program, if
3151 necessary. Here is a sample; alter the names:
3153 Yoyodyne, Inc., hereby disclaims all copyright interest in the program
3154 `Gnomovision' (which makes passes at compilers) written by James Hacker.
3156 <signature of Ty Coon>, 1 April 1989
3157 Ty Coon, President of Vice
3159 This General Public License does not permit incorporating your program into
3160 proprietary programs. If your program is a subroutine library, you may
3161 consider it more useful to permit linking proprietary applications with the
3162 library. If this is what you want to do, use the GNU Library General
3163 Public License instead of this License.
3167 def create_menu_and_toolbar
3170 mb = Gtk::MenuBar.new
3172 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
3173 filesubmenu = Gtk::Menu.new
3174 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
3175 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
3176 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3177 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
3178 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
3179 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3180 tooltips = Gtk::Tooltips.new
3181 filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
3182 $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3183 tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
3184 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
3185 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3186 tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums")), nil)
3187 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3188 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
3189 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3190 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
3191 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3192 filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
3193 tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
3194 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3195 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
3196 filemenu.set_submenu(filesubmenu)
3199 new.signal_connect('activate') { new_album }
3200 open.signal_connect('activate') { open_file_popup }
3201 $save.signal_connect('activate') { save_current_file_user }
3202 $save_as.signal_connect('activate') { save_as_do }
3203 $merge_current.signal_connect('activate') { merge_current }
3204 $merge.signal_connect('activate') { merge }
3205 $generate.signal_connect('activate') {
3207 call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
3208 utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
3210 { :successmsg => utf8(_("Your web-album is now ready in directory `%s'.
3211 Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
3212 :successmsg_linkurl => $xmldoc.root.attributes['destination'],
3213 :failuremsg => utf8(_("There was something wrong when generating the web-album, sorry.")),
3214 :closure_after => proc {
3215 $xmldoc.elements.each('//dir') { |elem|
3216 elem.add_attribute('already-generated', 'true')
3218 UndoHandler.cleanup #- prevent save_changes to mark current dir as not already generated
3219 $undo_tb.sensitive = $undo_mb.sensitive = false
3220 $redo_tb.sensitive = $redo_mb.sensitive = false
3222 $generated_outofline = true
3225 $properties.signal_connect('activate') { properties }
3227 quit.signal_connect('activate') { try_quit }
3229 editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
3230 editsubmenu = Gtk::Menu.new
3231 editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
3232 editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
3233 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3234 editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
3235 $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
3236 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)
3237 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3238 editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
3239 editmenu.set_submenu(editsubmenu)
3242 $remove_all_captions.signal_connect('activate') { remove_all_captions }
3244 prefs.signal_connect('activate') { preferences }
3246 helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
3247 helpsubmenu = Gtk::Menu.new
3248 helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
3249 one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
3250 helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
3251 speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
3252 helpsubmenu.append( Gtk::SeparatorMenuItem.new)
3253 helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
3254 helpmenu.set_submenu(helpsubmenu)
3257 one_click.signal_connect('activate') {
3258 show_one_click_explanation(_("One-Click tools are available in the toolbar."))
3261 speed.signal_connect('activate') {
3262 show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
3264 <span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
3265 <span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
3266 <span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
3267 <span foreground='darkblue'>Control-Enter</span>: for an image, open larger view; for a video, launch player
3268 <span foreground='darkblue'>Control-Delete</span>: delete image
3269 <span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
3270 <span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
3271 <span foreground='darkblue'>Control-z</span>: undo
3272 <span foreground='darkblue'>Control-r</span>: redo
3274 <span size='large' weight='bold'>Mouse gestures:</span>
3276 Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
3277 for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
3279 <span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
3280 <span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
3281 <span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
3282 <span foreground='darkblue'>Left click, hold left button, right click</span>: undo
3283 <span foreground='darkblue'>Right click, hold right button, left click</span>: redo
3284 ")), { :pos_centered => true, :not_transient => true })
3288 about.signal_connect('activate') {
3289 Gtk::AboutDialog.set_url_hook { |dialog, url| open_url(url) }
3290 Gtk::AboutDialog.show($main_window, { :name => 'booh',
3291 :version => $VERSION,
3292 :copyright => 'Copyright (c) 2005 Guillaume Cottenceau',
3293 :license => get_license,
3294 :website => 'http://zarb.org/~gc/html/booh.html',
3295 :authors => [ 'Guillaume Cottenceau' ],
3296 :artists => [ 'Ayo73' ],
3297 :comments => utf8(_("''The Web-Album of choice for discriminating Linux users''")),
3298 :translator_credits => utf8(_('Japanese: Masao Mutoh
3299 French: Guillaume Cottenceau')),
3300 :logo => Gdk::Pixbuf.new("#{$FPATH}/images/logo.png") })
3305 tb = Gtk::Toolbar.new
3307 tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
3308 open.label = utf8(_("Open")) #- to avoid missing gtk2 l10n catalogs
3309 open.menu = Gtk::Menu.new
3310 open.signal_connect('clicked') { open_file_popup }
3311 open.signal_connect('show-menu') {
3312 lastopens = Gtk::Menu.new
3314 if $config['last-opens']
3315 $config['last-opens'].reverse.each { |e|
3316 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
3317 item.signal_connect('activate') {
3318 if ask_save_modifications(utf8(_("Save this album?")),
3319 utf8(_("Do you want to save the changes to this album?")),
3320 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3321 push_mousecursor_wait
3322 msg = open_file_user(from_utf8(e))
3325 show_popup($main_window, msg)
3333 open.menu = lastopens
3336 tb.insert(-1, Gtk::SeparatorToolItem.new)
3338 tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
3339 $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
3340 $r90.label = utf8(_("Rotate"))
3341 tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
3342 $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
3343 $r270.label = utf8(_("Rotate"))
3344 tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
3345 $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
3346 $enhance.label = utf8(_("Enhance"))
3347 tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
3348 $delete.label = utf8(_("Delete")) #- to avoid missing gtk2 l10n catalogs
3349 tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
3350 nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
3351 nothing.label = utf8(_("None"))
3353 tb.insert(-1, Gtk::SeparatorToolItem.new)
3355 tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
3356 tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
3359 $undo_tb.signal_connect('clicked') { perform_undo }
3360 $undo_mb.signal_connect('activate') { perform_undo }
3361 $redo_tb.signal_connect('clicked') { perform_redo }
3362 $redo_mb.signal_connect('activate') { perform_redo }
3364 one_click_explain_try = Proc.new {
3365 if !$config['one-click-explained']
3366 show_one_click_explanation(_("You have just clicked on a One-Click tool."))
3367 $config['one-click-explained'] = true
3371 $r90.signal_connect('toggled') {
3373 set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
3374 one_click_explain_try.call
3375 $r270.active = false
3376 $enhance.active = false
3377 $delete.active = false
3378 nothing.sensitive = true
3380 if !$r270.active? && !$enhance.active? && !$delete.active?
3381 set_mousecursor_normal
3382 nothing.sensitive = false
3384 nothing.sensitive = true
3388 $r270.signal_connect('toggled') {
3390 set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
3391 one_click_explain_try.call
3393 $enhance.active = false
3394 $delete.active = false
3395 nothing.sensitive = true
3397 if !$r90.active? && !$enhance.active? && !$delete.active?
3398 set_mousecursor_normal
3399 nothing.sensitive = false
3401 nothing.sensitive = true
3405 $enhance.signal_connect('toggled') {
3407 set_mousecursor(Gdk::Cursor::SPRAYCAN)
3408 one_click_explain_try.call
3410 $r270.active = false
3411 $delete.active = false
3412 nothing.sensitive = true
3414 if !$r90.active? && !$r270.active? && !$delete.active?
3415 set_mousecursor_normal
3416 nothing.sensitive = false
3418 nothing.sensitive = true
3422 $delete.signal_connect('toggled') {
3424 set_mousecursor(Gdk::Cursor::PIRATE)
3425 one_click_explain_try.call
3427 $r270.active = false
3428 $enhance.active = false
3429 nothing.sensitive = true
3431 if !$r90.active? && !$r270.active? && !$enhance.active?
3432 set_mousecursor_normal
3433 nothing.sensitive = false
3435 nothing.sensitive = true
3439 nothing.signal_connect('clicked') {
3440 $r90.active = $r270.active = $enhance.active = $delete.active = false
3441 set_mousecursor_normal
3447 def gtk_thread_protect(&proc)
3448 if Thread.current == Thread.main
3451 $protect_gtk_pending_calls.synchronize {
3452 $gtk_pending_calls << proc
3457 def gtk_thread_abandon
3458 $protect_gtk_pending_calls.try_lock
3459 $gtk_pending_calls = []
3460 $protect_gtk_pending_calls.unlock
3463 def create_main_window
3465 mb, tb = create_menu_and_toolbar
3467 $albums_tv = Gtk::TreeView.new
3468 $albums_tv.set_size_request(120, -1)
3469 renderer = Gtk::CellRendererText.new
3470 column = Gtk::TreeViewColumn.new('', renderer, { :text => 0 })
3471 $albums_tv.append_column(column)
3472 $albums_tv.set_headers_visible(false)
3473 $albums_tv.selection.signal_connect('changed') { |w|
3474 push_mousecursor_wait
3478 msg 3, "no selection"
3480 $current_path = $albums_ts.get_value(iter, 1)
3485 $albums_ts = Gtk::TreeStore.new(String, String)
3486 $albums_tv.set_model($albums_ts)
3487 $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
3489 albums_sw = Gtk::ScrolledWindow.new(nil, nil)
3490 albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
3491 albums_sw.add_with_viewport($albums_tv)
3493 $notebook = Gtk::Notebook.new
3494 create_subalbums_page
3495 $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
3497 $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
3499 $notebook.signal_connect('switch-page') { |w, page, num|
3501 $delete.active = false
3502 $delete.sensitive = false
3504 $delete.sensitive = true
3506 if $xmldir && $subalbums_edits[$xmldir.attributes['path']] && textview = $subalbums_edits[$xmldir.attributes['path']][:editzone]
3508 textview.buffer.text = $thumbnails_title.buffer.text
3510 if $notebook.get_tab_label($autotable_sw).sensitive?
3511 $thumbnails_title.buffer.text = textview.buffer.text
3517 paned = Gtk::HPaned.new
3518 paned.pack1(albums_sw, false, false)
3519 paned.pack2($notebook, true, true)
3521 main_vbox = Gtk::VBox.new(false, 0)
3522 main_vbox.pack_start(mb, false, false)
3523 main_vbox.pack_start(tb, false, false)
3524 main_vbox.pack_start(paned, true, true)
3525 main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
3527 $main_window = Gtk::Window.new
3528 $main_window.add(main_vbox)
3529 $main_window.signal_connect('delete-event') {
3530 try_quit({ :disallow_cancel => true })
3533 #- read/save size and position of window
3534 if $config['pos-x'] && $config['pos-y']
3535 $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)