5 # A.k.a `Best web-album Of the world, Or your money back, Humerus'.
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
13 # Copyright (c) 2004 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
27 require 'booh/gtkadds'
28 require 'booh/GtkAutoTable'
32 bindtextdomain("booh")
34 require 'rexml/document'
37 require 'booh/booh-lib'
39 require 'booh/UndoHandler'
44 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
46 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
49 #- default values for some globals
53 $ignore_videos = false
54 $button1_pressed_autotable = false
55 $generated_outofline = false
58 puts _("Usage: %s [OPTION]...") % File.basename($0)
60 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
65 parser = GetoptLong.new
66 parser.set_options(*$options.collect { |ary| ary[0..2] })
68 parser.each_option do |name, arg|
74 when '--verbose-level'
75 $verbose_level = arg.to_i
88 $config_file = File.expand_path('~/.booh-gui-rc')
89 if File.readable?($config_file)
90 $xmldoc = REXML::Document.new(File.new($config_file))
91 $xmldoc.root.elements.each { |element|
92 txt = element.get_text
94 if txt.value =~ /~~~/ || element.name == 'last-opens'
95 $config[element.name] = txt.value.split(/~~~/)
97 $config[element.name] = txt.value
99 elsif element.elements.size == 0
100 $config[element.name] = ''
102 $config[element.name] = {}
103 element.each { |chld|
105 $config[element.name][chld.name] = txt ? txt.value : nil
110 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
111 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
112 if !FileTest.directory?(File.expand_path('~/.booh'))
113 system("mkdir ~/.booh")
121 if !system("which convert >/dev/null 2>/dev/null")
122 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
123 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
126 if !system("which identify >/dev/null 2>/dev/null")
127 show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
128 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
130 missing = %w(transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
132 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
135 viewer_binary = $config['video-viewer'].split.first
136 if viewer_binary && !File.executable?(viewer_binary)
137 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
138 You should fix this in Edit/Preferences so that you can view videos.
140 Problem was: '%s' is not an executable file.
141 Hint: don't forget to specify the full path to the executable,
142 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
144 browser_binary = $config['browser'].split.first
145 if browser_binary && !File.executable?(browser_binary)
146 show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
147 You should fix this in Edit/Preferences so that you can open URLs.
149 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
154 if $config['last-opens'] && $config['last-opens'].size > 5
155 $config['last-opens'] = $config['last-opens'][-5, 5]
158 ios = File.open($config_file, "w")
159 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
160 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
161 $config.each_pair { |key, value|
162 elem = $xmldoc.root.add_element key
164 $config[key].each_pair { |subkey, subvalue|
165 subelem = elem.add_element subkey
166 subelem.add_text subvalue.to_s
168 elsif value.is_a? Array
169 elem.add_text value.join('~~~')
174 elem.add_text value.to_s
178 $xmldoc.write(ios, 0)
181 $tempfiles.each { |f|
186 def set_mousecursor(what, *widget)
187 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
188 if widget[0] && widget[0].window
189 widget[0].window.cursor = cursor
191 if $main_window.window
192 $main_window.window.cursor = cursor
194 $current_cursor = what
196 def set_mousecursor_wait(*widget)
197 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
198 if Thread.current == Thread.main
199 Gtk.main_iteration while Gtk.events_pending?
202 def set_mousecursor_normal(*widget)
203 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
205 def push_mousecursor_wait(*widget)
206 if $current_cursor != Gdk::Cursor::WATCH
207 $save_cursor = $current_cursor
208 gtk_thread_protect { set_mousecursor_wait(*widget) }
211 def pop_mousecursor(*widget)
212 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
216 source = $xmldoc.root.attributes['source']
217 dest = $xmldoc.root.attributes['destination']
218 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
221 def full_src_dir_to_rel(path, source)
222 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
225 def build_full_dest_filename(filename)
226 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
229 def save_undo(name, closure, *params)
230 UndoHandler.save_undo(name, closure, [ *params ])
231 $undo_tb.sensitive = $undo_mb.sensitive = true
232 $redo_tb.sensitive = $redo_mb.sensitive = false
235 def view_element(filename, closures)
236 if entry2type(filename) == 'video'
237 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
243 w = Gtk::Window.new.set_title(filename)
245 msg 3, "filename: #{filename}"
246 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
247 #- typically this file won't exist in case of videos; try with the largest thumbnail around
248 if !File.exists?(dest_img)
249 if entry2type(filename) == 'video'
250 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
251 if not alternatives.empty?
252 dest_img = alternatives[-1]
255 push_mousecursor_wait
256 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
258 if !File.exists?(dest_img)
259 msg 2, _("Could not generate fullscreen thumbnail!")
264 evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(Gtk::Image.new(dest_img)).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
265 evt.signal_connect('button-press-event') { |this, event|
266 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
267 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
269 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
271 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
272 delete_item.signal_connect('activate') {
274 closures[:delete].call
277 menu.popup(nil, nil, event.button, event.time)
280 evt.signal_connect('button-release-event') { |this, event|
282 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
283 msg 3, "gesture delete: click-drag right button to the bottom"
285 closures[:delete].call
286 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
290 tooltips = Gtk::Tooltips.new
291 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
293 w.signal_connect('key-press-event') { |w,event|
294 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
296 closures[:delete].call
300 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
301 b.signal_connect('clicked') { w.destroy }
304 vb.pack_start(evt, false, false)
305 vb.pack_end(bottom, false, false)
308 w.signal_connect('delete-event') { w.destroy }
309 w.window_position = Gtk::Window::POS_CENTER
313 def scroll_upper(scrolledwindow, ypos_top)
314 newval = scrolledwindow.vadjustment.value -
315 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
316 if newval < scrolledwindow.vadjustment.lower
317 newval = scrolledwindow.vadjustment.lower
319 scrolledwindow.vadjustment.value = newval
322 def scroll_lower(scrolledwindow, ypos_bottom)
323 newval = scrolledwindow.vadjustment.value +
324 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
325 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
326 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
328 scrolledwindow.vadjustment.value = newval
331 def autoscroll_if_needed(scrolledwindow, image, textview)
332 #- autoscroll if cursor or image is not visible, if possible
333 if image && image.window || textview.window
334 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
335 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
336 current_miny_visible = scrolledwindow.vadjustment.value
337 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
338 if ypos_top < current_miny_visible
339 scroll_upper(scrolledwindow, ypos_top)
340 elsif ypos_bottom > current_maxy_visible
341 scroll_lower(scrolledwindow, ypos_bottom)
346 def create_editzone(scrolledwindow, pagenum, image)
347 frame = Gtk::Frame.new
348 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
349 frame.set_shadow_type(Gtk::SHADOW_IN)
350 textview.signal_connect('key-press-event') { |w, event|
351 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
352 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
353 scrolledwindow.signal_emit('key-press-event', event)
355 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
356 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
357 if event.keyval == Gdk::Keyval::GDK_Up
358 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
359 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
361 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
364 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
365 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
367 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
373 textview.signal_connect('focus-in-event') { |w, event|
374 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
378 candidate_undo_text = nil
379 textview.signal_connect('focus-in-event') { |w, event|
380 candidate_undo_text = textview.buffer.text
383 textview.signal_connect('key-release-event') { |w, event|
384 if candidate_undo_text && candidate_undo_text != textview.buffer.text
386 save_undo(_("text edit"),
388 save_text = textview.buffer.text
389 textview.buffer.text = text
391 $notebook.set_page(pagenum)
393 textview.buffer.text = save_text
395 $notebook.set_page(pagenum)
397 }, candidate_undo_text)
398 candidate_undo_text = nil
401 if event.state != 0 || ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
402 autoscroll_if_needed(scrolledwindow, image, textview)
407 return [ frame, textview ]
410 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
412 if !$modified_pixbufs[thumbnail_img]
413 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
414 elsif !$modified_pixbufs[thumbnail_img][:orig]
415 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
418 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
421 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
422 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
423 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
424 if pixbuf.height > desired_y
425 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
426 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
427 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
432 if $modified_pixbufs[thumbnail_img][:whitebalance]
433 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
436 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
439 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
442 #- update rotate attribute
443 xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
445 $modified_pixbufs[thumbnail_img] ||= {}
446 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
447 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
449 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
452 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
455 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
457 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
459 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
460 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
462 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
463 $notebook.set_page(0)
464 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
469 def color_swap(xmldir, attributes_prefix)
471 if xmldir.attributes["#{attributes_prefix}color-swap"]
472 xmldir.delete_attribute("#{attributes_prefix}color-swap")
474 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
478 def enhance(xmldir, attributes_prefix)
480 if xmldir.attributes["#{attributes_prefix}enhance"]
481 xmldir.delete_attribute("#{attributes_prefix}enhance")
483 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
487 def change_frame_offset(xmldir, attributes_prefix, value)
489 xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
492 def ask_new_frame_offset(xmldir, attributes_prefix)
494 value = xmldir.attributes["#{attributes_prefix}frame-offset"]
499 dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
501 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
502 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
503 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
507 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
508 from. There are approximately 25 frames per second in a video.
511 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
512 entry.signal_connect('key-press-event') { |w, event|
513 if event.keyval == Gdk::Keyval::GDK_Return
514 dialog.response(Gtk::Dialog::RESPONSE_OK)
516 elsif event.keyval == Gdk::Keyval::GDK_Escape
517 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
520 false #- propagate if needed
524 dialog.window_position = Gtk::Window::POS_MOUSE
527 dialog.run { |response|
530 if response == Gtk::Dialog::RESPONSE_OK
532 msg 3, "changing frame offset to #{newval}"
533 return { :old => value, :new => newval }
540 def change_pano_amount(xmldir, attributes_prefix, value)
543 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
545 xmldir.add_attribute("#{attributes_prefix}pano-amount", value)
549 def ask_new_pano_amount(xmldir, attributes_prefix)
551 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
556 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
558 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
559 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
560 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
564 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
565 of this panorama image compared to other regular images. For example, if the panorama
566 was taken out of four photos on one row, counting the necessary overlap, the width of
567 this panorama image should probably be roughly three times the width of regular images.
569 With this information, booh will be able to generate panorama thumbnails looking
573 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("none (not a panorama image)")))).
574 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
575 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
576 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
577 dialog.window_position = Gtk::Window::POS_MOUSE
580 spin.value = value.to_f
587 dialog.run { |response|
591 newval = spin.value.to_f
594 if response == Gtk::Dialog::RESPONSE_OK
596 msg 3, "changing panorama amount to #{newval}"
597 return { :old => value, :new => newval }
604 def change_whitebalance(xmlelem, attributes_prefix, value)
606 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
609 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
611 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
612 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
613 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
614 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
615 destfile = "#{thumbnail_img}-orig-whitebalance.jpg"
616 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
617 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
618 $modified_pixbufs[thumbnail_img] ||= {}
619 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
620 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
621 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
624 $modified_pixbufs[thumbnail_img] ||= {}
625 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
627 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
630 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
631 #- init $modified_pixbufs correctly
632 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
634 value = xmlelem.attributes["#{attributes_prefix}white-balance"] || "0"
636 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
638 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
639 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
640 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
644 _("You can fix the <b>white balance</b> of the image, if your image is too blue
645 or too yellow because your camera didn't detect the light correctly. Drag the
646 slider below the image to the left for more blue, to the right for more yellow.
649 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
650 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
652 dialog.window_position = Gtk::Window::POS_MOUSE
656 timeout = Gtk.timeout_add(100) {
657 if hs.value != lastval
659 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
664 dialog.run { |response|
665 Gtk.timeout_remove(timeout)
666 if response == Gtk::Dialog::RESPONSE_OK
668 newval = hs.value.to_s
669 msg 3, "changing white balance to #{newval}"
671 return { :old => value, :new => newval }
673 $modified_pixbufs[thumbnail_img] ||= {}
674 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
675 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
682 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
683 system("rm -f '#{destfile}'")
684 #- type can be 'element' or 'subdir'
686 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
688 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
692 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
694 push_mousecursor_wait
695 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
697 puts "destroyed: " + img.destroyed?.to_s
699 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
705 def popup_thumbnail_menu(event, optionals, type, xmldir, attributes_prefix, possible_actions, closures)
706 distribute_multiple_call = Proc.new { |action, arg|
707 $selected_elements.each_key { |path|
708 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
710 if possible_actions[:can_multiple] && $selected_elements.length > 0
711 UndoHandler.begin_batch
712 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
713 UndoHandler.end_batch
715 closures[action].call(arg)
717 $selected_elements = {}
720 if optionals.include?('change_image')
721 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
722 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
723 changeimg.signal_connect('activate') { closures[:change].call }
724 menu.append( Gtk::SeparatorMenuItem.new)
728 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
729 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
730 view.signal_connect('activate') { closures[:view].call }
731 menu.append( Gtk::SeparatorMenuItem.new)
733 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
734 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
735 view.signal_connect('activate') { closures[:view].call }
736 menu.append( Gtk::SeparatorMenuItem.new)
739 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
740 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
741 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
742 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
743 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
744 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
745 if !possible_actions[:can_multiple] || $selected_elements.length == 0
746 menu.append( Gtk::SeparatorMenuItem.new)
747 if !possible_actions[:forbid_left]
748 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
749 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
750 moveleft.signal_connect('activate') { closures[:move].call('left') }
751 if !possible_actions[:can_left]
752 moveleft.sensitive = false
755 if !possible_actions[:forbid_right]
756 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
757 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
758 moveright.signal_connect('activate') { closures[:move].call('right') }
759 if !possible_actions[:can_right]
760 moveright.sensitive = false
763 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
764 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
765 moveup.signal_connect('activate') { closures[:move].call('up') }
766 if !possible_actions[:can_up]
767 moveup.sensitive = false
769 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
770 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
771 movedown.signal_connect('activate') { closures[:move].call('down') }
772 if !possible_actions[:can_down]
773 movedown.sensitive = false
777 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
778 menu.append( Gtk::SeparatorMenuItem.new)
779 menu.append( color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
780 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
781 color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
782 menu.append( flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
783 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
784 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
785 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
786 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
787 frame_offset.signal_connect('activate') {
788 if possible_actions[:can_multiple] && $selected_elements.length > 0
789 if values = ask_new_frame_offset(nil, '')
790 distribute_multiple_call.call(:frame_offset, values)
793 closures[:frame_offset].call
798 menu.append( Gtk::SeparatorMenuItem.new)
799 if !possible_actions[:can_multiple] || $selected_elements.length == 0
800 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
801 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
802 whitebalance.signal_connect('activate') { closures[:whitebalance].call }
804 if !possible_actions[:can_multiple] || $selected_elements.length == 0
805 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
806 _("Enhance constrast"))))
808 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
810 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
811 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
812 if type == 'image' && possible_actions[:can_panorama]
813 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
814 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
815 panorama.signal_connect('activate') { closures[:pano].call }
817 if optionals.include?('delete')
818 menu.append( Gtk::SeparatorMenuItem.new)
819 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
820 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
821 if !possible_actions[:can_multiple] || $selected_elements.length == 0
822 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
823 paste_item.signal_connect('activate') { closures[:paste].call }
824 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
825 clear_item.signal_connect('activate') { $cuts = [] }
827 paste_item.sensitive = clear_item.sensitive = false
830 menu.append( Gtk::SeparatorMenuItem.new)
831 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
832 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
835 menu.popup(nil, nil, event.button, event.time)
838 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
841 frame1 = Gtk::Frame.new
842 fullpath = from_utf8("#{$current_path}/#{filename}")
844 my_gen_real_thumbnail = proc {
845 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
848 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
849 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
850 frame1.add(img = Gtk::Image.new)
851 my_gen_real_thumbnail.call
853 frame1.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
855 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
857 tooltips = Gtk::Tooltips.new
858 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
859 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
861 frame2, textview = create_editzone($autotable_sw, 1, img)
862 textview.buffer.text = utf8(caption)
863 textview.set_justification(Gtk::Justification::CENTER)
865 vbox = Gtk::VBox.new(false, 5)
866 vbox.pack_start(evtbox, false, false)
867 vbox.pack_start(frame2, false, false)
868 autotable.append(vbox, filename)
870 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
871 $vbox2widgets[vbox] = { :textview => textview, :image => img }
873 #- to be able to find widgets by name
874 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
876 cleanup_all_thumbnails = Proc.new {
877 #- remove out of sync images
878 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
879 for sizeobj in $images_size
880 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
885 rotate_and_cleanup = Proc.new { |angle|
886 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
887 cleanup_all_thumbnails.call
890 move = Proc.new { |direction|
891 do_method = "move_#{direction}"
892 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
894 done = autotable.method(do_method).call(vbox)
895 textview.grab_focus #- because if moving, focus is stolen
899 save_undo(_("move %s") % direction,
901 autotable.method(undo_method).call(vbox)
902 textview.grab_focus #- because if moving, focus is stolen
903 autoscroll_if_needed($autotable_sw, img, textview)
904 $notebook.set_page(1)
906 autotable.method(do_method).call(vbox)
907 textview.grab_focus #- because if moving, focus is stolen
908 autoscroll_if_needed($autotable_sw, img, textview)
909 $notebook.set_page(1)
915 color_swap_and_cleanup = Proc.new {
916 perform_color_swap_and_cleanup = Proc.new {
917 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
918 my_gen_real_thumbnail.call
921 cleanup_all_thumbnails.call
922 perform_color_swap_and_cleanup.call
924 save_undo(_("color swap"),
926 perform_color_swap_and_cleanup.call
928 autoscroll_if_needed($autotable_sw, img, textview)
929 $notebook.set_page(1)
931 perform_color_swap_and_cleanup.call
933 autoscroll_if_needed($autotable_sw, img, textview)
934 $notebook.set_page(1)
939 change_frame_offset_and_cleanup_real = Proc.new { |values|
940 perform_change_frame_offset_and_cleanup = Proc.new { |val|
941 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
942 my_gen_real_thumbnail.call
944 perform_change_frame_offset_and_cleanup.call(values[:new])
946 save_undo(_("specify frame offset"),
948 perform_change_frame_offset_and_cleanup.call(values[:old])
950 autoscroll_if_needed($autotable_sw, img, textview)
951 $notebook.set_page(1)
953 perform_change_frame_offset_and_cleanup.call(values[:new])
955 autoscroll_if_needed($autotable_sw, img, textview)
956 $notebook.set_page(1)
961 change_frame_offset_and_cleanup = Proc.new {
962 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
963 change_frame_offset_and_cleanup_real.call(values)
967 change_pano_amount_and_cleanup_real = Proc.new { |values|
968 perform_change_pano_amount_and_cleanup = Proc.new { |val|
969 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
971 perform_change_pano_amount_and_cleanup.call(values[:new])
973 save_undo(_("change panorama amount"),
975 perform_change_pano_amount_and_cleanup.call(values[:old])
977 autoscroll_if_needed($autotable_sw, img, textview)
978 $notebook.set_page(1)
980 perform_change_pano_amount_and_cleanup.call(values[:new])
982 autoscroll_if_needed($autotable_sw, img, textview)
983 $notebook.set_page(1)
988 change_pano_amount_and_cleanup = Proc.new {
989 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
990 change_pano_amount_and_cleanup_real.call(values)
994 whitebalance_and_cleanup = Proc.new {
995 if values = ask_whitebalance(fullpath, thumbnail_img, img,
996 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
997 perform_change_whitebalance_and_cleanup = Proc.new { |val|
998 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
999 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1000 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1001 cleanup_all_thumbnails.call
1003 perform_change_whitebalance_and_cleanup.call(values[:new])
1005 save_undo(_("fix white balance"),
1007 perform_change_whitebalance_and_cleanup.call(values[:old])
1009 autoscroll_if_needed($autotable_sw, img, textview)
1010 $notebook.set_page(1)
1012 perform_change_whitebalance_and_cleanup.call(values[:new])
1014 autoscroll_if_needed($autotable_sw, img, textview)
1015 $notebook.set_page(1)
1021 enhance_and_cleanup = Proc.new {
1022 perform_enhance_and_cleanup = Proc.new {
1023 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1024 my_gen_real_thumbnail.call
1027 cleanup_all_thumbnails.call
1028 perform_enhance_and_cleanup.call
1030 save_undo(_("enhance"),
1032 perform_enhance_and_cleanup.call
1034 autoscroll_if_needed($autotable_sw, img, textview)
1035 $notebook.set_page(1)
1037 perform_enhance_and_cleanup.call
1039 autoscroll_if_needed($autotable_sw, img, textview)
1040 $notebook.set_page(1)
1045 delete = Proc.new { |isacut|
1046 if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed?")), { :okcancel => true })
1049 perform_delete = Proc.new {
1050 after = autotable.get_next_widget(vbox)
1052 after = autotable.get_previous_widget(vbox)
1054 if $config['deleteondisk'] && !isacut
1055 msg 3, "scheduling for delete: #{fullpath}"
1056 $todelete << fullpath
1058 autotable.remove(vbox)
1060 $vbox2widgets[after][:textview].grab_focus
1061 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1065 previous_pos = autotable.get_current_number(vbox)
1069 if $xmldir.child_byname_notattr('dir', 'deleted')
1070 $xmldir.delete_attribute('thumbnails-caption')
1071 $xmldir.delete_attribute('thumbnails-captionfile')
1073 $xmldir.add_attribute('deleted', 'true')
1075 while moveup.parent.name == 'dir'
1076 moveup = moveup.parent
1077 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1078 moveup.add_attribute('deleted', 'true')
1084 save_changes('forced')
1085 populate_subalbums_treeview
1087 save_undo(_("delete"),
1089 autotable.reinsert(pos, vbox, filename)
1090 $notebook.set_page(1)
1091 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1093 msg 3, "removing deletion schedule of: #{fullpath}"
1094 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1097 $notebook.set_page(1)
1106 $cuts << { :vbox => vbox, :filename => filename }
1107 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1112 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1115 autotable.queue_draws << proc {
1116 $vbox2widgets[last[:vbox]][:textview].grab_focus
1117 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1119 save_undo(_("paste"),
1121 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1122 $notebook.set_page(1)
1125 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1127 $notebook.set_page(1)
1130 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1135 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1136 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1137 :pano => change_pano_amount_and_cleanup }
1139 textview.signal_connect('key-press-event') { |w, event|
1142 x, y = autotable.get_current_pos(vbox)
1143 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1144 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1145 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1146 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1148 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1149 $vbox2widgets[widget_up][:textview].grab_focus
1156 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1158 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1159 $vbox2widgets[widget_down][:textview].grab_focus
1166 if event.keyval == Gdk::Keyval::GDK_Left
1169 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1176 rotate_and_cleanup.call(-90)
1179 if event.keyval == Gdk::Keyval::GDK_Right
1180 next_ = autotable.get_next_widget(vbox)
1181 if next_ && autotable.get_current_pos(next_)[0] > x
1183 $vbox2widgets[next_][:textview].grab_focus
1190 rotate_and_cleanup.call(90)
1193 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1196 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1197 view_element(filename, { :delete => delete })
1200 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1203 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1207 !propagate #- propagate if needed
1210 $ignore_next_release = false
1211 evtbox.signal_connect('button-press-event') { |w, event|
1212 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1213 if event.state & Gdk::Window::BUTTON3_MASK != 0
1214 #- gesture redo: hold right mouse button then click left mouse button
1215 $config['nogestures'] or perform_redo
1216 $ignore_next_release = true
1218 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1220 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1222 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1223 elsif $enhance.active?
1224 enhance_and_cleanup.call
1225 elsif $delete.active?
1229 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1232 $button1_pressed_autotable = true
1233 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1234 if event.state & Gdk::Window::BUTTON1_MASK != 0
1235 #- gesture undo: hold left mouse button then click right mouse button
1236 $config['nogestures'] or perform_undo
1237 $ignore_next_release = true
1239 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1240 view_element(filename, { :delete => delete })
1245 evtbox.signal_connect('button-release-event') { |w, event|
1246 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1247 if !$ignore_next_release
1248 x, y = autotable.get_current_pos(vbox)
1249 next_ = autotable.get_next_widget(vbox)
1250 popup_thumbnail_menu(event, ['delete'], type, $xmldir.elements["*[@filename='#{filename}']"], '',
1251 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1252 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1253 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1254 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1255 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1256 :pano => change_pano_amount_and_cleanup })
1258 $ignore_next_release = false
1259 $gesture_press = nil
1264 #- handle reordering with drag and drop
1265 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1266 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1267 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1268 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1271 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1273 #- mouse gesture first (dnd disables button-release-event)
1274 if $gesture_press && $gesture_press[:filename] == filename
1275 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1276 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1277 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1278 rotate_and_cleanup.call(angle)
1279 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1281 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1282 msg 3, "gesture delete: click-drag right button to the bottom"
1284 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1289 ctxt.targets.each { |target|
1290 if target.name == 'reorder-elements'
1291 move_dnd = Proc.new { |from,to|
1294 autotable.move(from, to)
1295 save_undo(_("reorder"),
1296 Proc.new { |from, to|
1298 autotable.move(to - 1, from)
1300 autotable.move(to, from + 1)
1302 $notebook.set_page(1)
1304 autotable.move(from, to)
1305 $notebook.set_page(1)
1310 if $multiple_dnd.size == 0
1311 move_dnd.call(selection_data.data.to_i,
1312 autotable.get_current_number(vbox))
1314 UndoHandler.begin_batch
1315 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1317 #- need to update current position between each call
1318 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1319 autotable.get_current_number(vbox))
1321 UndoHandler.end_batch
1332 def create_auto_table
1334 $autotable = Gtk::AutoTable.new(5)
1336 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1337 thumbnails_vb = Gtk::VBox.new(false, 5)
1339 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1340 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1341 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1342 thumbnails_vb.add($autotable)
1344 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1345 $autotable_sw.add_with_viewport(thumbnails_vb)
1347 #- follows stuff for handling multiple elements selection
1348 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1350 update_selected = Proc.new {
1351 $autotable.current_order.each { |path|
1352 w = $name2widgets[path][:evtbox].window
1353 xm = w.position[0] + w.size[0]/2
1354 ym = w.position[1] + w.size[1]/2
1355 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1356 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1357 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1358 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1361 if $selected_elements[path] && ! $selected_elements[path][:keep]
1362 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))
1363 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1364 $selected_elements.delete(path)
1369 $autotable.signal_connect('realize') { |w,e|
1370 gc = Gdk::GC.new($autotable.window)
1371 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1372 gc.function = Gdk::GC::INVERT
1373 #- autoscroll handling for DND and multiple selections
1374 Gtk.timeout_add(100) {
1375 w, x, y, mask = $autotable.window.pointer
1376 if mask & Gdk::Window::BUTTON1_MASK != 0
1377 if y < $autotable_sw.vadjustment.value
1379 $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]])
1381 if $button1_pressed_autotable || press_x
1382 scroll_upper($autotable_sw, y)
1385 w, pos_x, pos_y = $autotable.window.pointer
1386 $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]])
1387 update_selected.call
1390 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1392 $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]])
1394 if $button1_pressed_autotable || press_x
1395 scroll_lower($autotable_sw, y)
1398 w, pos_x, pos_y = $autotable.window.pointer
1399 $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]])
1400 update_selected.call
1408 $autotable.signal_connect('button-press-event') { |w,e|
1410 if !$button1_pressed_autotable
1413 if e.state & Gdk::Window::SHIFT_MASK == 0
1414 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1415 $selected_elements = {}
1416 $statusbar.push(0, utf8(_("Nothing selected.")))
1418 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1420 set_mousecursor(Gdk::Cursor::TCROSS)
1424 $autotable.signal_connect('button-release-event') { |w,e|
1426 if $button1_pressed_autotable
1427 #- unselect all only now
1428 $multiple_dnd = $selected_elements.keys
1429 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1430 $selected_elements = {}
1431 $button1_pressed_autotable = false
1434 $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]])
1435 if $selected_elements.length > 0
1436 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1439 press_x = press_y = pos_x = pos_y = nil
1440 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1444 $autotable.signal_connect('motion-notify-event') { |w,e|
1447 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1451 $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]])
1452 update_selected.call
1458 def create_subalbums_page
1460 subalbums_hb = Gtk::HBox.new
1461 $subalbums_vb = Gtk::VBox.new(false, 5)
1462 subalbums_hb.pack_start($subalbums_vb, false, false)
1463 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1464 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1465 $subalbums_sw.add_with_viewport(subalbums_hb)
1468 def save_current_file
1472 ios = File.open($filename, "w")
1473 $xmldoc.write(ios, 0)
1478 def save_current_file_user
1479 save_tempfilename = $filename
1480 $filename = $orig_filename
1483 $generated_outofline = false
1484 $filename = save_tempfilename
1486 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1487 $todelete.each { |f|
1488 system("rm -f #{f}")
1492 def mark_document_as_dirty
1493 $xmldoc.elements.each('//dir') { |elem|
1494 elem.delete_attribute('already-generated')
1498 #- ret: true => ok false => cancel
1499 def ask_save_modifications(msg1, msg2, *options)
1501 options = options.size > 0 ? options[0] : {}
1503 if options[:disallow_cancel]
1504 dialog = Gtk::Dialog.new(msg1,
1506 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1507 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1508 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1510 dialog = Gtk::Dialog.new(msg1,
1512 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1513 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1514 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1515 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1517 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1518 dialog.vbox.add(Gtk::Label.new(msg2))
1519 dialog.window_position = Gtk::Window::POS_CENTER
1522 dialog.run { |response|
1524 if response == Gtk::Dialog::RESPONSE_YES
1525 save_current_file_user
1527 #- if we have generated an album but won't save modifications, we must remove
1528 #- already-generated markers in original file
1529 if $generated_outofline
1531 $xmldoc = REXML::Document.new File.new($orig_filename)
1532 mark_document_as_dirty
1533 ios = File.open($orig_filename, "w")
1534 $xmldoc.write(ios, 0)
1537 puts "exception: #{$!}"
1541 if response == Gtk::Dialog::RESPONSE_CANCEL
1544 $todelete = [] #- unconditionally clear the list of images/videos to delete
1550 def try_quit(*options)
1551 if ask_save_modifications(utf8(_("Save before quitting?")),
1552 utf8(_("Do you want to save your changes before quitting?")),
1558 def show_popup(parent, msg, *options)
1559 dialog = Gtk::Dialog.new
1560 dialog.title = utf8(_("Booh message"))
1561 lbl = Gtk::Label.new
1563 if options[0] && options[0][:centered]
1564 lbl.set_justify(Gtk::Justification::CENTER)
1566 if options[0] && options[0][:selectable]
1567 lbl.selectable = true
1569 if options[0] && options[0][:topwidget]
1570 dialog.vbox.add(options[0][:topwidget])
1572 dialog.vbox.add(lbl)
1573 if options[0] && options[0][:okcancel]
1574 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1576 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1578 dialog.set_default_size(200, 120)
1579 if options[0] && options[0][:pos_centered]
1580 dialog.window_position = Gtk::Window::POS_CENTER
1582 dialog.window_position = Gtk::Window::POS_MOUSE
1585 if options[0] && options[0][:linkurl]
1586 linkbut = Gtk::Button.new('')
1587 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1588 linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1589 linkbut.relief = Gtk::RELIEF_NONE
1590 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1591 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1592 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1597 if !options[0] || !options[0][:not_transient]
1598 dialog.transient_for = parent
1599 dialog.run { |response|
1601 if options[0] && options[0][:okcancel]
1602 return response == Gtk::Dialog::RESPONSE_OK
1606 dialog.signal_connect('response') { dialog.destroy }
1610 def backend_wait_message(parent, msg, infopipe_path, mode)
1612 w.set_transient_for(parent)
1615 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1616 vb.pack_start(Gtk::Label.new(msg), false, false)
1618 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1619 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1620 if mode != 'one dir scan'
1621 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1623 if mode == 'web-album'
1624 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1625 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1627 vb.pack_start(Gtk::HSeparator.new, false, false)
1629 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1630 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1631 vb.pack_end(bottom, false, false)
1633 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1634 refresh_thread = Thread.new {
1635 directories_counter = 0
1636 while line = infopipe.gets
1637 if line =~ /^directories: (\d+), sizes: (\d+)/
1638 directories = $1.to_f + 1
1640 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1641 elements = $3.to_f + 1
1642 if mode == 'web-album'
1646 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = 0 }
1647 if mode != 'one dir scan'
1648 newtext = utf8(full_src_dir_to_rel($1, $2))
1649 newtext = '/' if newtext == ''
1650 gtk_thread_protect { puts "destroyed: " + pb1_2.destroyed?.to_s; pb1_2.text = newtext }
1651 directories_counter += 1
1652 gtk_thread_protect { puts "destroyed: " + pb1_2.destroyed?.to_s; pb1_2.fraction = directories_counter / directories }
1654 elsif line =~ /^processing element$/
1655 element_counter += 1
1656 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = element_counter / elements }
1657 elsif line =~ /^processing size$/
1658 element_counter += 1
1659 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = element_counter / elements }
1660 elsif line =~ /^finished processing sizes$/
1661 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = 1 }
1662 elsif line =~ /^creating index.html$/
1663 gtk_thread_protect { puts "destroyed: " + pb1_2.destroyed?.to_s; pb1_2.text = utf8(_("finished")) }
1664 gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = pb1_2.fraction = 1 }
1665 directories_counter = 0
1666 elsif line =~ /^index.html: (.+)\|(.+)/
1667 newtext = utf8(full_src_dir_to_rel($1, $2))
1668 newtext = '/' if newtext == ''
1669 gtk_thread_protect { puts "destroyed: " + pb2.destroyed?.to_s; pb2.text = newtext }
1670 directories_counter += 1
1671 gtk_thread_protect { puts "destroyed: " + pb2.destroyed?.to_s; pb2.fraction = directories_counter / directories }
1677 w.signal_connect('delete-event') { w.destroy }
1678 w.signal_connect('destroy') {
1679 Thread.kill(refresh_thread)
1680 gtk_thread_abandon #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1683 system("rm -f #{infopipe_path}")
1686 w.window_position = Gtk::Window::POS_CENTER
1692 def call_backend(cmd, waitmsg, mode, params)
1693 pipe = Tempfile.new("boohpipe")
1695 system("mkfifo #{pipe.path}")
1696 cmd += " --info-pipe #{pipe.path}"
1697 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
1702 id, exitstatus = Process.waitpid2(pid)
1703 gtk_thread_protect { puts "destroyed: " + w8.destroyed?.to_s; w8.destroy }
1705 if params[:successmsg]
1706 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
1708 if params[:closure_after]
1709 gtk_thread_protect(¶ms[:closure_after])
1711 elsif exitstatus == 15
1712 #- say nothing, user aborted
1714 if params[:failuremsg]
1715 gtk_thread_protect { show_popup($main_window, params[:failuremsg]) }
1722 button.signal_connect('clicked') {
1723 Process.kill('SIGTERM', pid)
1727 def save_changes(*forced)
1728 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1732 $xmldir.delete_attribute('already-generated')
1734 propagate_children = Proc.new { |xmldir|
1735 if xmldir.attributes['subdirs-caption']
1736 xmldir.delete_attribute('already-generated')
1738 xmldir.elements.each('dir') { |element|
1739 propagate_children.call(element)
1743 if $xmldir.child_byname_notattr('dir', 'deleted')
1744 new_title = $subalbums_title.buffer.text
1745 if new_title != $xmldir.attributes['subdirs-caption']
1746 parent = $xmldir.parent
1747 if parent.name == 'dir'
1748 parent.delete_attribute('already-generated')
1750 propagate_children.call($xmldir)
1752 $xmldir.add_attribute('subdirs-caption', new_title)
1753 $xmldir.elements.each('dir') { |element|
1754 if !element.attributes['deleted']
1755 path = element.attributes['path']
1756 newtext = $subalbums_edits[path][:editzone].buffer.text
1757 if element.attributes['subdirs-caption']
1758 if element.attributes['subdirs-caption'] != newtext
1759 propagate_children.call(element)
1761 element.add_attribute('subdirs-caption', newtext)
1762 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1764 if element.attributes['thumbnails-caption'] != newtext
1765 element.delete_attribute('already-generated')
1767 element.add_attribute('thumbnails-caption', newtext)
1768 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1774 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
1775 if $xmldir.attributes['thumbnails-caption']
1776 path = $xmldir.attributes['path']
1777 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1779 elsif $xmldir.attributes['thumbnails-caption']
1780 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
1783 #- remove and reinsert elements to reflect new ordering
1786 $xmldir.elements.each { |element|
1787 if element.name == 'image' || element.name == 'video'
1788 saves[element.attributes['filename']] = element.remove
1792 $autotable.current_order.each { |path|
1793 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1794 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1797 saves.each_key { |path|
1798 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1799 chld.add_attribute('deleted', 'true')
1803 def remove_all_captions
1806 $autotable.current_order.each { |path|
1807 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
1808 $name2widgets[File.basename(path)][:textview].buffer.text = ''
1810 save_undo(_("remove all captions"),
1812 texts.each_key { |key|
1813 $name2widgets[key][:textview].buffer.text = texts[key]
1815 $notebook.set_page(1)
1817 texts.each_key { |key|
1818 $name2widgets[key][:textview].buffer.text = ''
1820 $notebook.set_page(1)
1826 $selected_elements.each_key { |path|
1827 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1833 $selected_elements = {}
1837 $undo_tb.sensitive = $undo_mb.sensitive = false
1838 $redo_tb.sensitive = $redo_mb.sensitive = false
1844 $subalbums_vb.children.each { |chld|
1845 $subalbums_vb.remove(chld)
1847 $subalbums = Gtk::Table.new(0, 0, true)
1848 current_y_sub_albums = 0
1850 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
1851 $subalbums_edits = {}
1852 subalbums_counter = 0
1853 subalbums_edits_bypos = {}
1855 add_subalbum = Proc.new { |xmldir, counter|
1856 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
1857 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
1858 if xmldir == $xmldir
1859 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
1860 caption = xmldir.attributes['thumbnails-caption']
1861 captionfile, dummy = find_subalbum_caption_info(xmldir)
1862 infotype = 'thumbnails'
1864 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
1865 captionfile, caption = find_subalbum_caption_info(xmldir)
1866 infotype = find_subalbum_info_type(xmldir)
1868 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
1869 hbox = Gtk::HBox.new
1870 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
1872 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
1875 my_gen_real_thumbnail = proc {
1876 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
1879 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
1880 f.add(img = Gtk::Image.new)
1881 my_gen_real_thumbnail.call
1883 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
1885 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
1886 $subalbums.attach(hbox,
1887 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1889 frame, textview = create_editzone($subalbums_sw, 0, img)
1890 textview.buffer.text = caption
1891 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
1892 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1894 change_image = Proc.new {
1895 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
1897 Gtk::FileChooser::ACTION_OPEN,
1899 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1900 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
1901 fc.transient_for = $main_window
1902 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))
1903 f.add(preview_img = Gtk::Image.new)
1905 fc.signal_connect('update-preview') { |w|
1907 if fc.preview_filename
1908 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
1909 fc.preview_widget_active = true
1911 rescue Gdk::PixbufError
1912 fc.preview_widget_active = false
1915 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1917 old_file = captionfile
1918 old_rotate = xmldir.attributes["#{infotype}-rotate"]
1919 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
1920 old_enhance = xmldir.attributes["#{infotype}-enhance"]
1921 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
1923 new_file = fc.filename
1924 msg 3, "new captionfile is: #{fc.filename}"
1925 perform_changefile = Proc.new {
1926 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
1927 $modified_pixbufs.delete(thumbnail_file)
1928 xmldir.delete_attribute("#{infotype}-rotate")
1929 xmldir.delete_attribute("#{infotype}-color-swap")
1930 xmldir.delete_attribute("#{infotype}-enhance")
1931 xmldir.delete_attribute("#{infotype}-frame-offset")
1932 my_gen_real_thumbnail.call
1934 perform_changefile.call
1936 save_undo(_("change caption file for sub-album"),
1938 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
1939 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
1940 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
1941 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
1942 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
1943 my_gen_real_thumbnail.call
1944 $notebook.set_page(0)
1946 perform_changefile.call
1947 $notebook.set_page(0)
1954 rotate_and_cleanup = Proc.new { |angle|
1955 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
1956 system("rm -f '#{thumbnail_file}'")
1959 move = Proc.new { |direction|
1962 save_changes('forced')
1963 if direction == 'up'
1964 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
1965 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
1966 subalbums_edits_bypos[oldpos - 1][:position] += 1
1968 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
1969 $subalbums_edits[xmldir.attributes['path']][:position] += 1
1970 subalbums_edits_bypos[oldpos + 1][:position] -= 1
1974 $xmldir.elements.each('dir') { |element|
1975 if (!element.attributes['deleted'])
1976 elems << [ element.attributes['path'], element.remove ]
1979 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
1980 each { |e| $xmldir.add_element(e[1]) }
1981 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
1982 $xmldir.elements.each('descendant::dir') { |elem|
1983 elem.delete_attribute('already-generated')
1988 color_swap_and_cleanup = Proc.new {
1989 perform_color_swap_and_cleanup = Proc.new {
1990 color_swap(xmldir, "#{infotype}-")
1991 my_gen_real_thumbnail.call
1993 perform_color_swap_and_cleanup.call
1995 save_undo(_("color swap"),
1997 perform_color_swap_and_cleanup.call
1998 $notebook.set_page(0)
2000 perform_color_swap_and_cleanup.call
2001 $notebook.set_page(0)
2006 change_frame_offset_and_cleanup = Proc.new {
2007 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2008 perform_change_frame_offset_and_cleanup = Proc.new { |val|
2009 change_frame_offset(xmldir, "#{infotype}-", val)
2010 my_gen_real_thumbnail.call
2012 perform_change_frame_offset_and_cleanup.call(values[:new])
2014 save_undo(_("specify frame offset"),
2016 perform_change_frame_offset_and_cleanup.call(values[:old])
2017 $notebook.set_page(0)
2019 perform_change_frame_offset_and_cleanup.call(values[:new])
2020 $notebook.set_page(0)
2026 whitebalance_and_cleanup = Proc.new {
2027 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2028 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2029 perform_change_whitebalance_and_cleanup = Proc.new { |val|
2030 change_whitebalance(xmldir, "#{infotype}-", val)
2031 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2032 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2033 system("rm -f '#{thumbnail_file}'")
2035 perform_change_whitebalance_and_cleanup.call(values[:new])
2037 save_undo(_("fix white balance"),
2039 perform_change_whitebalance_and_cleanup.call(values[:old])
2040 $notebook.set_page(0)
2042 perform_change_whitebalance_and_cleanup.call(values[:new])
2043 $notebook.set_page(0)
2049 enhance_and_cleanup = Proc.new {
2050 perform_enhance_and_cleanup = Proc.new {
2051 enhance(xmldir, "#{infotype}-")
2052 my_gen_real_thumbnail.call
2055 perform_enhance_and_cleanup.call
2057 save_undo(_("enhance"),
2059 perform_enhance_and_cleanup.call
2060 $notebook.set_page(0)
2062 perform_enhance_and_cleanup.call
2063 $notebook.set_page(0)
2068 evtbox.signal_connect('button-press-event') { |w, event|
2069 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2071 rotate_and_cleanup.call(90)
2073 rotate_and_cleanup.call(-90)
2074 elsif $enhance.active?
2075 enhance_and_cleanup.call
2078 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2079 popup_thumbnail_menu(event, ['change_image'], entry2type(captionfile), xmldir, "#{infotype}-",
2080 { :forbid_left => true, :forbid_right => true,
2081 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter },
2082 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2083 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
2085 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2090 evtbox.signal_connect('button-press-event') { |w, event|
2091 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2095 evtbox.signal_connect('button-release-event') { |w, event|
2096 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2097 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2098 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2099 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2100 msg 3, "gesture rotate: #{angle}"
2101 rotate_and_cleanup.call(angle)
2104 $gesture_press = nil
2107 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2108 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2109 current_y_sub_albums += 1
2112 if $xmldir.child_byname_notattr('dir', 'deleted')
2114 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2115 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2116 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2117 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2118 #- this album image/caption
2119 if $xmldir.attributes['thumbnails-caption']
2120 add_subalbum.call($xmldir, 0)
2123 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2124 $xmldir.elements.each { |element|
2125 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2126 #- element (image or video) of this album
2127 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2128 msg 3, "dest_img: #{dest_img}"
2129 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
2130 total[element.name] += 1
2132 if element.name == 'dir' && !element.attributes['deleted']
2133 #- sub-album image/caption
2134 add_subalbum.call(element, subalbums_counter += 1)
2135 total[element.name] += 1
2138 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2139 total['image'], total['video'], total['dir'] ]))
2140 $subalbums_vb.add($subalbums)
2141 $subalbums_vb.show_all
2143 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2144 $notebook.get_tab_label($autotable_sw).sensitive = false
2145 $notebook.set_page(0)
2146 $thumbnails_title.buffer.text = ''
2148 $notebook.get_tab_label($autotable_sw).sensitive = true
2149 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2152 if !$xmldir.child_byname_notattr('dir', 'deleted')
2153 $notebook.get_tab_label($subalbums_sw).sensitive = false
2154 $notebook.set_page(1)
2156 $notebook.get_tab_label($subalbums_sw).sensitive = true
2160 def pixbuf_or_nil(filename)
2162 return Gdk::Pixbuf.new(filename)
2168 def theme_choose(current)
2169 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2171 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2172 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2173 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2175 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2176 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2177 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2178 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2179 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2180 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2181 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2182 treeview.signal_connect('button-press-event') { |w, event|
2183 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2184 dialog.response(Gtk::Dialog::RESPONSE_OK)
2188 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2190 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2193 iter[0] = File.basename(dir)
2194 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2195 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2196 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2197 if File.basename(dir) == current
2198 treeview.selection.select_iter(iter)
2202 dialog.set_default_size(700, 400)
2203 dialog.vbox.show_all
2204 dialog.run { |response|
2205 iter = treeview.selection.selected
2207 if response == Gtk::Dialog::RESPONSE_OK && iter
2208 return model.get_value(iter, 0)
2214 def show_password_protections
2215 examine_dir_elem = Proc.new { |parent_iter, xmldir, already_protected|
2216 child_iter = $albums_iters[xmldir.attributes['path']]
2217 if xmldir.attributes['password-protect']
2218 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2219 already_protected = true
2220 elsif already_protected
2221 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2223 pix = pix.saturate_and_pixelate(1, true)
2229 xmldir.elements.each('dir') { |elem|
2230 if !elem.attributes['deleted']
2231 examine_dir_elem.call(child_iter, elem, already_protected)
2235 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2238 def populate_subalbums_treeview
2242 $subalbums_vb.children.each { |chld|
2243 $subalbums_vb.remove(chld)
2246 source = $xmldoc.root.attributes['source']
2247 msg 3, "source: #{source}"
2249 xmldir = $xmldoc.elements['//dir']
2250 if !xmldir || xmldir.attributes['path'] != source
2251 msg 1, _("Corrupted booh file...")
2255 append_dir_elem = Proc.new { |parent_iter, xmldir|
2256 child_iter = $albums_ts.append(parent_iter)
2257 child_iter[0] = File.basename(xmldir.attributes['path'])
2258 child_iter[1] = xmldir.attributes['path']
2259 $albums_iters[xmldir.attributes['path']] = child_iter
2260 msg 3, "puttin location: #{xmldir.attributes['path']}"
2261 xmldir.elements.each('dir') { |elem|
2262 if !elem.attributes['deleted']
2263 append_dir_elem.call(child_iter, elem)
2267 append_dir_elem.call(nil, xmldir)
2268 show_password_protections
2270 $albums_tv.expand_all
2271 $albums_tv.selection.select_iter($albums_ts.iter_first)
2274 def open_file(filename)
2278 $current_path = nil #- invalidate
2279 $modified_pixbufs = {}
2282 $subalbums_vb.children.each { |chld|
2283 $subalbums_vb.remove(chld)
2286 if !File.exists?(filename)
2287 return utf8(_("File not found."))
2291 $xmldoc = REXML::Document.new File.new(filename)
2296 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2297 if entry2type(filename).nil?
2298 return utf8(_("Not a booh file!"))
2300 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."))
2304 if !source = $xmldoc.root.attributes['source']
2305 return utf8(_("Corrupted booh file..."))
2308 if !dest = $xmldoc.root.attributes['destination']
2309 return utf8(_("Corrupted booh file..."))
2312 if !theme = $xmldoc.root.attributes['theme']
2313 return utf8(_("Corrupted booh file..."))
2316 if $xmldoc.root.attributes['version'] != $VERSION
2317 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2318 mark_document_as_dirty
2319 $xmldoc.root.add_attribute('version', $VERSION)
2322 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2323 optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2324 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2326 $filename = filename
2327 select_theme(theme, limit_sizes, optimizefor32, nperrow)
2328 $default_size['thumbnails'] =~ /(.*)x(.*)/
2329 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2330 $albums_thumbnail_size =~ /(.*)x(.*)/
2331 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2333 populate_subalbums_treeview
2335 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = true
2339 def open_file_user(filename)
2340 result = open_file(filename)
2342 $config['last-opens'] ||= []
2343 if $config['last-opens'][-1] != utf8(filename)
2344 $config['last-opens'] << utf8(filename)
2346 $orig_filename = $filename
2347 tmp = Tempfile.new("boohtemp")
2350 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2352 $tempfiles << $filename << "#{$filename}.backup"
2354 $orig_filename = nil
2360 if !ask_save_modifications(utf8(_("Save this album?")),
2361 utf8(_("Do you want to save the changes to this album?")),
2362 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2365 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2367 Gtk::FileChooser::ACTION_OPEN,
2369 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2370 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2371 fc.set_current_folder(File.expand_path("~/.booh"))
2372 fc.transient_for = $main_window
2375 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2376 push_mousecursor_wait(fc)
2377 msg = open_file_user(fc.filename)
2393 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2398 def additional_booh_options
2401 options += "--mproc #{$config['mproc'].to_i} "
2403 if $config['emptycomments']
2404 options += "--empty-comments "
2410 if !ask_save_modifications(utf8(_("Save this album?")),
2411 utf8(_("Do you want to save the changes to this album?")),
2412 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2415 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2417 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2418 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2419 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2421 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2422 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2423 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2424 tbl.attach(src = Gtk::Entry.new,
2425 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2426 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2427 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2428 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2429 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2430 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2431 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2432 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2433 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2434 tbl.attach(dest = Gtk::Entry.new,
2435 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2436 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2437 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2438 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2439 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2440 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2441 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2442 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2443 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2445 tooltips = Gtk::Tooltips.new
2446 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2447 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2448 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2449 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2450 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2451 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2452 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)
2453 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2454 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2455 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2456 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2457 tooltips.set_tip(madewithentry, utf8(_("Optional HTML markup to use on pages bottom for a small 'made with' label; %booh is replaced by the website of booh;\nfor example: made with <a href=%booh>booh</a>!")), nil)
2459 src_nb_calculated_for = ''
2461 process_src_nb = Proc.new {
2462 if src.text != src_nb_calculated_for
2463 src_nb_calculated_for = src.text
2465 Thread.kill(src_nb_thread)
2468 if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2469 if File.readable?(from_utf8(src_nb_calculated_for))
2470 src_nb_thread = Thread.new {
2471 gtk_thread_protect { puts "destroyed: " + src_nb.destroyed?.to_s; src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2472 total = { 'image' => 0, 'video' => 0, nil => 0 }
2473 `find '#{from_utf8(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2474 if File.basename(dir) =~ /^\./
2478 Dir.entries(dir.chomp).each { |file|
2479 total[entry2type(file)] += 1
2481 rescue Errno::EACCES, Errno::ENOENT
2485 gtk_thread_protect { puts "destroyed: " + src_nb.destroyed?.to_s; src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2489 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2492 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2497 timeout_src_nb = Gtk.timeout_add(100) {
2501 src_browse.signal_connect('clicked') {
2502 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2504 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2506 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2507 fc.transient_for = $main_window
2508 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2509 src.text = utf8(fc.filename)
2511 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2516 dest_browse.signal_connect('clicked') {
2517 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2519 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2521 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2522 fc.transient_for = $main_window
2523 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2524 dest.text = utf8(fc.filename)
2529 conf_browse.signal_connect('clicked') {
2530 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2532 Gtk::FileChooser::ACTION_SAVE,
2534 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2535 fc.transient_for = $main_window
2536 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2537 fc.set_current_folder(File.expand_path("~/.booh"))
2538 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2539 conf.text = utf8(fc.filename)
2546 recreate_theme_config = proc {
2547 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2549 select_theme(theme_button.label, 'all', optimize432.active?, nil)
2550 $images_size.each { |s|
2551 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2555 tooltips.set_tip(cb, utf8(s['description']), nil)
2556 theme_sizes << { :widget => cb, :value => s['name'] }
2558 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2559 tooltips = Gtk::Tooltips.new
2560 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2561 theme_sizes << { :widget => cb, :value => 'original' }
2564 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2567 $allowed_N_values.each { |n|
2569 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2571 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2573 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2577 nperrows << { :widget => rb, :value => n }
2579 nperrowradios.show_all
2581 recreate_theme_config.call
2583 theme_button.signal_connect('clicked') {
2584 if newtheme = theme_choose(theme_button.label)
2585 theme_button.label = newtheme
2586 recreate_theme_config.call
2590 dialog.vbox.add(frame1)
2591 dialog.vbox.add(frame2)
2592 dialog.window_position = Gtk::Window::POS_MOUSE
2598 dialog.run { |response|
2599 if response == Gtk::Dialog::RESPONSE_OK
2600 srcdir = from_utf8(src.text)
2601 destdir = from_utf8(dest.text)
2602 if !File.directory?(srcdir)
2603 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2605 elsif conf.text == ''
2606 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2608 elsif File.directory?(from_utf8(conf.text))
2609 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
2611 elsif destdir != make_dest_filename(destdir)
2612 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2614 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
2615 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2617 elsif File.exists?(destdir) && !File.directory?(destdir)
2618 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2620 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2621 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2623 system("mkdir '#{destdir}'")
2624 if !File.directory?(destdir)
2625 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2636 srcdir = from_utf8(src.text)
2637 destdir = from_utf8(dest.text)
2638 configskel = File.expand_path(from_utf8(conf.text))
2639 theme = theme_button.label
2640 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2641 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2642 opt432 = optimize432.active?
2643 madewith = madewithentry.text
2645 Thread.kill(src_nb_thread)
2646 gtk_thread_abandon #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2649 Gtk.timeout_remove(timeout_src_nb)
2652 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2653 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2654 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' #{additional_booh_options}",
2655 utf8(_("Please wait while scanning source directory...")),
2657 { :closure_after => proc { open_file_user(configskel) } })
2662 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2664 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2665 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2666 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2668 source = $xmldoc.root.attributes['source']
2669 dest = $xmldoc.root.attributes['destination']
2670 theme = $xmldoc.root.attributes['theme']
2671 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2672 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2673 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2675 limit_sizes = limit_sizes.split(/,/)
2677 madewith = $xmldoc.root.attributes['made-with']
2679 tooltips = Gtk::Tooltips.new
2680 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2681 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2682 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2683 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2684 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2685 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2686 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2687 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
2688 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2689 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2690 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2691 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
2692 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2694 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2695 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2696 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2697 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2698 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2699 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2700 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)
2701 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2702 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2703 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2704 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2706 madewithentry.text = madewith
2708 tooltips.set_tip(madewithentry, utf8(_("Optional HTML markup to use on pages bottom for a small 'made with' label; %booh is replaced by the website of booh;\nfor example: made with <a href=%booh>booh</a>!")), nil)
2712 recreate_theme_config = proc {
2713 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2715 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
2717 $images_size.each { |s|
2718 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2720 if limit_sizes.include?(s['name'])
2728 tooltips.set_tip(cb, utf8(s['description']), nil)
2729 theme_sizes << { :widget => cb, :value => s['name'] }
2731 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2732 tooltips = Gtk::Tooltips.new
2733 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2734 if limit_sizes && limit_sizes.include?('original')
2737 theme_sizes << { :widget => cb, :value => 'original' }
2740 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2743 $allowed_N_values.each { |n|
2745 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2747 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2749 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2750 nperrowradios.add(Gtk::Label.new(' '))
2751 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
2754 nperrows << { :widget => rb, :value => n.to_s }
2756 nperrowradios.show_all
2758 recreate_theme_config.call
2760 theme_button.signal_connect('clicked') {
2761 if newtheme = theme_choose(theme_button.label)
2764 theme_button.label = newtheme
2765 recreate_theme_config.call
2769 dialog.vbox.add(frame1)
2770 dialog.vbox.add(frame2)
2771 dialog.window_position = Gtk::Window::POS_MOUSE
2777 dialog.run { |response|
2778 if response == Gtk::Dialog::RESPONSE_OK
2779 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2780 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2789 save_theme = theme_button.label
2790 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
2791 save_opt432 = optimize432.active?
2792 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2793 save_madewith = madewithentry.text
2796 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith)
2797 mark_document_as_dirty
2799 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
2800 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
2801 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' #{additional_booh_options}",
2802 utf8(_("Please wait while scanning source directory...")),
2804 { :closure_after => proc {
2805 open_file($filename)
2814 sel = $albums_tv.selection.selected_rows
2816 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2817 "--verbose-level #{$verbose_level} #{additional_booh_options}",
2818 utf8(_("Please wait while scanning source directory...")),
2820 { :closure_after => proc {
2821 open_file($filename)
2822 $albums_tv.selection.select_path(sel[0])
2830 sel = $albums_tv.selection.selected_rows
2832 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2833 "--verbose-level #{$verbose_level} #{additional_booh_options}",
2834 utf8(_("Please wait while scanning source directory...")),
2836 { :closure_after => proc {
2837 open_file($filename)
2838 $albums_tv.selection.select_path(sel[0])
2846 theme = $xmldoc.root.attributes['theme']
2847 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2849 limit_sizes = "--sizes #{limit_sizes}"
2851 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
2852 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
2853 utf8(_("Please wait while scanning source directory...")),
2855 { :closure_after => proc {
2856 open_file($filename)
2862 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
2864 Gtk::FileChooser::ACTION_SAVE,
2866 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2867 fc.transient_for = $main_window
2868 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2869 fc.set_current_folder(File.expand_path("~/.booh"))
2870 fc.filename = $orig_filename
2871 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2872 $orig_filename = fc.filename
2873 save_current_file_user
2879 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
2881 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2882 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2883 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2885 dialog.vbox.add(notebook = Gtk::Notebook.new)
2886 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
2887 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
2888 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2889 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
2890 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2891 tooltips = Gtk::Tooltips.new
2892 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
2893 for example: /usr/bin/mplayer %f")), nil)
2894 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
2895 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2896 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
2897 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2898 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
2899 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
2900 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
2901 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2902 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)),
2903 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2904 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)
2905 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
2906 0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2907 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)
2908 tbl.attach(emptycomments_check = Gtk::CheckButton.new(utf8(_("Use empty comments for new albums"))),
2909 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
2910 tooltips.set_tip(emptycomments_check, utf8(_("Normally, filenames are used as comments for new albums. Check this if you prefer empty comments.")), nil)
2911 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
2912 0, 2, 5, 6, Gtk::FILL, Gtk::SHRINK, 2, 2)
2913 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)
2914 smp_check.signal_connect('toggled') {
2915 if smp_check.active?
2916 smp_hbox.sensitive = true
2918 smp_hbox.sensitive = false
2922 smp_check.active = true
2923 smp_spin.value = $config['mproc'].to_i
2925 nogestures_check.active = $config['nogestures']
2926 emptycomments_check.active = $config['emptycomments']
2927 deleteondisk_check.active = $config['deleteondisk']
2929 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
2930 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
2931 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2932 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
2933 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2935 dialog.vbox.show_all
2936 dialog.run { |response|
2937 if response == Gtk::Dialog::RESPONSE_OK
2938 $config['video-viewer'] = video_viewer_entry.text
2939 $config['browser'] = browser_entry.text
2940 if smp_check.active?
2941 $config['mproc'] = smp_spin.value.to_i
2943 $config.delete('mproc')
2945 $config['nogestures'] = nogestures_check.active?
2946 $config['emptycomments'] = emptycomments_check.active?
2947 $config['deleteondisk'] = deleteondisk_check.active?
2949 $config['convert-enhance'] = enhance_entry.text
2956 if $undo_tb.sensitive?
2957 $redo_tb.sensitive = $redo_mb.sensitive = true
2958 if not more_undoes = UndoHandler.undo($statusbar)
2959 $undo_tb.sensitive = $undo_mb.sensitive = false
2965 if $redo_tb.sensitive?
2966 $undo_tb.sensitive = $undo_mb.sensitive = true
2967 if not more_redoes = UndoHandler.redo($statusbar)
2968 $redo_tb.sensitive = $redo_mb.sensitive = false
2973 def show_one_click_explanation(intro)
2974 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
2976 %s When such a tool is activated
2977 (<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
2978 on a thumbnail will immediately apply the desired action.
2980 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
2986 GNU GENERAL PUBLIC LICENSE
2987 Version 2, June 1991
2989 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
2990 675 Mass Ave, Cambridge, MA 02139, USA
2991 Everyone is permitted to copy and distribute verbatim copies
2992 of this license document, but changing it is not allowed.
2996 The licenses for most software are designed to take away your
2997 freedom to share and change it. By contrast, the GNU General Public
2998 License is intended to guarantee your freedom to share and change free
2999 software--to make sure the software is free for all its users. This
3000 General Public License applies to most of the Free Software
3001 Foundation's software and to any other program whose authors commit to
3002 using it. (Some other Free Software Foundation software is covered by
3003 the GNU Library General Public License instead.) You can apply it to
3006 When we speak of free software, we are referring to freedom, not
3007 price. Our General Public Licenses are designed to make sure that you
3008 have the freedom to distribute copies of free software (and charge for
3009 this service if you wish), that you receive source code or can get it
3010 if you want it, that you can change the software or use pieces of it
3011 in new free programs; and that you know you can do these things.
3013 To protect your rights, we need to make restrictions that forbid
3014 anyone to deny you these rights or to ask you to surrender the rights.
3015 These restrictions translate to certain responsibilities for you if you
3016 distribute copies of the software, or if you modify it.
3018 For example, if you distribute copies of such a program, whether
3019 gratis or for a fee, you must give the recipients all the rights that
3020 you have. You must make sure that they, too, receive or can get the
3021 source code. And you must show them these terms so they know their
3024 We protect your rights with two steps: (1) copyright the software, and
3025 (2) offer you this license which gives you legal permission to copy,
3026 distribute and/or modify the software.
3028 Also, for each author's protection and ours, we want to make certain
3029 that everyone understands that there is no warranty for this free
3030 software. If the software is modified by someone else and passed on, we
3031 want its recipients to know that what they have is not the original, so
3032 that any problems introduced by others will not reflect on the original
3033 authors' reputations.
3035 Finally, any free program is threatened constantly by software
3036 patents. We wish to avoid the danger that redistributors of a free
3037 program will individually obtain patent licenses, in effect making the
3038 program proprietary. To prevent this, we have made it clear that any
3039 patent must be licensed for everyone's free use or not licensed at all.
3041 The precise terms and conditions for copying, distribution and
3042 modification follow.
3045 GNU GENERAL PUBLIC LICENSE
3046 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
3048 0. This License applies to any program or other work which contains
3049 a notice placed by the copyright holder saying it may be distributed
3050 under the terms of this General Public License. The "Program", below,
3051 refers to any such program or work, and a "work based on the Program"
3052 means either the Program or any derivative work under copyright law:
3053 that is to say, a work containing the Program or a portion of it,
3054 either verbatim or with modifications and/or translated into another
3055 language. (Hereinafter, translation is included without limitation in
3056 the term "modification".) Each licensee is addressed as "you".
3058 Activities other than copying, distribution and modification are not
3059 covered by this License; they are outside its scope. The act of
3060 running the Program is not restricted, and the output from the Program
3061 is covered only if its contents constitute a work based on the
3062 Program (independent of having been made by running the Program).
3063 Whether that is true depends on what the Program does.
3065 1. You may copy and distribute verbatim copies of the Program's
3066 source code as you receive it, in any medium, provided that you
3067 conspicuously and appropriately publish on each copy an appropriate
3068 copyright notice and disclaimer of warranty; keep intact all the
3069 notices that refer to this License and to the absence of any warranty;
3070 and give any other recipients of the Program a copy of this License
3071 along with the Program.
3073 You may charge a fee for the physical act of transferring a copy, and
3074 you may at your option offer warranty protection in exchange for a fee.
3076 2. You may modify your copy or copies of the Program or any portion
3077 of it, thus forming a work based on the Program, and copy and
3078 distribute such modifications or work under the terms of Section 1
3079 above, provided that you also meet all of these conditions:
3081 a) You must cause the modified files to carry prominent notices
3082 stating that you changed the files and the date of any change.
3084 b) You must cause any work that you distribute or publish, that in
3085 whole or in part contains or is derived from the Program or any
3086 part thereof, to be licensed as a whole at no charge to all third
3087 parties under the terms of this License.
3089 c) If the modified program normally reads commands interactively
3090 when run, you must cause it, when started running for such
3091 interactive use in the most ordinary way, to print or display an
3092 announcement including an appropriate copyright notice and a
3093 notice that there is no warranty (or else, saying that you provide
3094 a warranty) and that users may redistribute the program under
3095 these conditions, and telling the user how to view a copy of this
3096 License. (Exception: if the Program itself is interactive but
3097 does not normally print such an announcement, your work based on
3098 the Program is not required to print an announcement.)
3101 These requirements apply to the modified work as a whole. If
3102 identifiable sections of that work are not derived from the Program,
3103 and can be reasonably considered independent and separate works in
3104 themselves, then this License, and its terms, do not apply to those
3105 sections when you distribute them as separate works. But when you
3106 distribute the same sections as part of a whole which is a work based
3107 on the Program, the distribution of the whole must be on the terms of
3108 this License, whose permissions for other licensees extend to the
3109 entire whole, and thus to each and every part regardless of who wrote it.
3111 Thus, it is not the intent of this section to claim rights or contest
3112 your rights to work written entirely by you; rather, the intent is to
3113 exercise the right to control the distribution of derivative or
3114 collective works based on the Program.
3116 In addition, mere aggregation of another work not based on the Program
3117 with the Program (or with a work based on the Program) on a volume of
3118 a storage or distribution medium does not bring the other work under
3119 the scope of this License.
3121 3. You may copy and distribute the Program (or a work based on it,
3122 under Section 2) in object code or executable form under the terms of
3123 Sections 1 and 2 above provided that you also do one of the following:
3125 a) Accompany it with the complete corresponding machine-readable
3126 source code, which must be distributed under the terms of Sections
3127 1 and 2 above on a medium customarily used for software interchange; or,
3129 b) Accompany it with a written offer, valid for at least three
3130 years, to give any third party, for a charge no more than your
3131 cost of physically performing source distribution, a complete
3132 machine-readable copy of the corresponding source code, to be
3133 distributed under the terms of Sections 1 and 2 above on a medium
3134 customarily used for software interchange; or,
3136 c) Accompany it with the information you received as to the offer
3137 to distribute corresponding source code. (This alternative is
3138 allowed only for noncommercial distribution and only if you
3139 received the program in object code or executable form with such
3140 an offer, in accord with Subsection b above.)
3142 The source code for a work means the preferred form of the work for
3143 making modifications to it. For an executable work, complete source
3144 code means all the source code for all modules it contains, plus any
3145 associated interface definition files, plus the scripts used to
3146 control compilation and installation of the executable. However, as a
3147 special exception, the source code distributed need not include
3148 anything that is normally distributed (in either source or binary
3149 form) with the major components (compiler, kernel, and so on) of the
3150 operating system on which the executable runs, unless that component
3151 itself accompanies the executable.
3153 If distribution of executable or object code is made by offering
3154 access to copy from a designated place, then offering equivalent
3155 access to copy the source code from the same place counts as
3156 distribution of the source code, even though third parties are not
3157 compelled to copy the source along with the object code.
3160 4. You may not copy, modify, sublicense, or distribute the Program
3161 except as expressly provided under this License. Any attempt
3162 otherwise to copy, modify, sublicense or distribute the Program is
3163 void, and will automatically terminate your rights under this License.
3164 However, parties who have received copies, or rights, from you under
3165 this License will not have their licenses terminated so long as such
3166 parties remain in full compliance.
3168 5. You are not required to accept this License, since you have not
3169 signed it. However, nothing else grants you permission to modify or
3170 distribute the Program or its derivative works. These actions are
3171 prohibited by law if you do not accept this License. Therefore, by
3172 modifying or distributing the Program (or any work based on the
3173 Program), you indicate your acceptance of this License to do so, and
3174 all its terms and conditions for copying, distributing or modifying
3175 the Program or works based on it.
3177 6. Each time you redistribute the Program (or any work based on the
3178 Program), the recipient automatically receives a license from the
3179 original licensor to copy, distribute or modify the Program subject to
3180 these terms and conditions. You may not impose any further
3181 restrictions on the recipients' exercise of the rights granted herein.
3182 You are not responsible for enforcing compliance by third parties to
3185 7. If, as a consequence of a court judgment or allegation of patent
3186 infringement or for any other reason (not limited to patent issues),
3187 conditions are imposed on you (whether by court order, agreement or
3188 otherwise) that contradict the conditions of this License, they do not
3189 excuse you from the conditions of this License. If you cannot
3190 distribute so as to satisfy simultaneously your obligations under this
3191 License and any other pertinent obligations, then as a consequence you
3192 may not distribute the Program at all. For example, if a patent
3193 license would not permit royalty-free redistribution of the Program by
3194 all those who receive copies directly or indirectly through you, then
3195 the only way you could satisfy both it and this License would be to
3196 refrain entirely from distribution of the Program.
3198 If any portion of this section is held invalid or unenforceable under
3199 any particular circumstance, the balance of the section is intended to
3200 apply and the section as a whole is intended to apply in other
3203 It is not the purpose of this section to induce you to infringe any
3204 patents or other property right claims or to contest validity of any
3205 such claims; this section has the sole purpose of protecting the
3206 integrity of the free software distribution system, which is
3207 implemented by public license practices. Many people have made
3208 generous contributions to the wide range of software distributed
3209 through that system in reliance on consistent application of that
3210 system; it is up to the author/donor to decide if he or she is willing
3211 to distribute software through any other system and a licensee cannot
3214 This section is intended to make thoroughly clear what is believed to
3215 be a consequence of the rest of this License.
3218 8. If the distribution and/or use of the Program is restricted in
3219 certain countries either by patents or by copyrighted interfaces, the
3220 original copyright holder who places the Program under this License
3221 may add an explicit geographical distribution limitation excluding
3222 those countries, so that distribution is permitted only in or among
3223 countries not thus excluded. In such case, this License incorporates
3224 the limitation as if written in the body of this License.
3226 9. The Free Software Foundation may publish revised and/or new versions
3227 of the General Public License from time to time. Such new versions will
3228 be similar in spirit to the present version, but may differ in detail to
3229 address new problems or concerns.
3231 Each version is given a distinguishing version number. If the Program
3232 specifies a version number of this License which applies to it and "any
3233 later version", you have the option of following the terms and conditions
3234 either of that version or of any later version published by the Free
3235 Software Foundation. If the Program does not specify a version number of
3236 this License, you may choose any version ever published by the Free Software
3239 10. If you wish to incorporate parts of the Program into other free
3240 programs whose distribution conditions are different, write to the author
3241 to ask for permission. For software which is copyrighted by the Free
3242 Software Foundation, write to the Free Software Foundation; we sometimes
3243 make exceptions for this. Our decision will be guided by the two goals
3244 of preserving the free status of all derivatives of our free software and
3245 of promoting the sharing and reuse of software generally.
3249 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
3250 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
3251 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
3252 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
3253 OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
3254 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
3255 TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
3256 PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
3257 REPAIR OR CORRECTION.
3259 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
3260 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
3261 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
3262 INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
3263 OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
3264 TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
3265 YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
3266 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
3267 POSSIBILITY OF SUCH DAMAGES.
3271 def create_menu_and_toolbar
3274 mb = Gtk::MenuBar.new
3276 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
3277 filesubmenu = Gtk::Menu.new
3278 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
3279 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
3280 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3281 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
3282 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
3283 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3284 tooltips = Gtk::Tooltips.new
3285 filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
3286 $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3287 tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
3288 filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
3289 $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3290 tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
3291 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
3292 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3293 tooltips.set_tip($merge, utf8(_("Take into account new subalbums (subdirectories) and new/removed images/videos in existing subalbums (anywhere)")), nil)
3294 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3295 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
3296 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3297 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
3298 filesubmenu.append($view_wa = Gtk::ImageMenuItem.new(utf8(_("View web-album with browser"))).set_sensitive(false))
3299 $view_wa.image = Gtk::Image.new("#{$FPATH}/images/stock-view-webalbum-16.png")
3300 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3301 filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
3302 tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
3303 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3304 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
3305 filemenu.set_submenu(filesubmenu)
3308 new.signal_connect('activate') { new_album }
3309 open.signal_connect('activate') { open_file_popup }
3310 $save.signal_connect('activate') { save_current_file_user }
3311 $save_as.signal_connect('activate') { save_as_do }
3312 $merge_current.signal_connect('activate') { merge_current }
3313 $merge_newsubs.signal_connect('activate') { merge_newsubs }
3314 $merge.signal_connect('activate') { merge }
3315 $generate.signal_connect('activate') {
3317 call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
3318 utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
3320 { :successmsg => utf8(_("Your web-album is now ready in directory `%s'.
3321 Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
3322 :successmsg_linkurl => $xmldoc.root.attributes['destination'],
3323 :failuremsg => utf8(_("There was something wrong when generating the web-album, sorry.")),
3324 :closure_after => proc {
3325 $xmldoc.elements.each('//dir') { |elem|
3326 elem.add_attribute('already-generated', 'true')
3328 UndoHandler.cleanup #- prevent save_changes to mark current dir as not already generated
3329 $undo_tb.sensitive = $undo_mb.sensitive = false
3330 $redo_tb.sensitive = $redo_mb.sensitive = false
3332 $generated_outofline = true
3335 $view_wa.signal_connect('activate') {
3336 indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
3337 if File.exists?(indexhtml)
3340 show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
3343 $properties.signal_connect('activate') { properties }
3345 quit.signal_connect('activate') { try_quit }
3347 editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
3348 editsubmenu = Gtk::Menu.new
3349 editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
3350 editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
3351 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3352 editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
3353 $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
3354 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)
3355 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3356 editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
3357 editmenu.set_submenu(editsubmenu)
3360 $remove_all_captions.signal_connect('activate') { remove_all_captions }
3362 prefs.signal_connect('activate') { preferences }
3364 helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
3365 helpsubmenu = Gtk::Menu.new
3366 helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
3367 one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
3368 helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
3369 speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
3370 helpsubmenu.append( Gtk::SeparatorMenuItem.new)
3371 helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
3372 helpmenu.set_submenu(helpsubmenu)
3375 one_click.signal_connect('activate') {
3376 show_one_click_explanation(_("One-Click tools are available in the toolbar."))
3379 speed.signal_connect('activate') {
3380 show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
3382 <span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
3383 <span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
3384 <span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
3385 <span foreground='darkblue'>Control-Enter</span>: for an image, open larger view; for a video, launch player
3386 <span foreground='darkblue'>Control-Delete</span>: delete image
3387 <span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
3388 <span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
3389 <span foreground='darkblue'>Control-z</span>: undo
3390 <span foreground='darkblue'>Control-r</span>: redo
3392 <span size='large' weight='bold'>Mouse gestures:</span>
3394 Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
3395 for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
3397 <span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
3398 <span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
3399 <span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
3400 <span foreground='darkblue'>Left click, hold left button, right click</span>: undo
3401 <span foreground='darkblue'>Right click, hold right button, left click</span>: redo
3402 ")), { :pos_centered => true, :not_transient => true })
3406 about.signal_connect('activate') {
3407 Gtk::AboutDialog.set_url_hook { |dialog, url| open_url(url) }
3408 Gtk::AboutDialog.show($main_window, { :name => 'booh',
3409 :version => $VERSION,
3410 :copyright => 'Copyright (c) 2005 Guillaume Cottenceau',
3411 :license => get_license,
3412 :website => 'http://zarb.org/~gc/html/booh.html',
3413 :authors => [ 'Guillaume Cottenceau' ],
3414 :artists => [ 'Ayo73' ],
3415 :comments => utf8(_("''The Web-Album of choice for discriminating Linux users''")),
3416 :translator_credits => utf8(_('Japanese: Masao Mutoh
3417 German: Roland Eckert
3418 French: Guillaume Cottenceau')),
3419 :logo => Gdk::Pixbuf.new("#{$FPATH}/images/logo.png") })
3424 tb = Gtk::Toolbar.new
3426 tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
3427 open.label = utf8(_("Open")) #- to avoid missing gtk2 l10n catalogs
3428 open.menu = Gtk::Menu.new
3429 open.signal_connect('clicked') { open_file_popup }
3430 open.signal_connect('show-menu') {
3431 lastopens = Gtk::Menu.new
3433 if $config['last-opens']
3434 $config['last-opens'].reverse.each { |e|
3435 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
3436 item.signal_connect('activate') {
3437 if ask_save_modifications(utf8(_("Save this album?")),
3438 utf8(_("Do you want to save the changes to this album?")),
3439 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3440 push_mousecursor_wait
3441 msg = open_file_user(from_utf8(e))
3444 show_popup($main_window, msg)
3452 open.menu = lastopens
3455 tb.insert(-1, Gtk::SeparatorToolItem.new)
3457 tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
3458 $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
3459 $r90.label = utf8(_("Rotate"))
3460 tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
3461 $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
3462 $r270.label = utf8(_("Rotate"))
3463 tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
3464 $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
3465 $enhance.label = utf8(_("Enhance"))
3466 tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
3467 $delete.label = utf8(_("Delete")) #- to avoid missing gtk2 l10n catalogs
3468 tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
3469 nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
3470 nothing.label = utf8(_("None"))
3472 tb.insert(-1, Gtk::SeparatorToolItem.new)
3474 tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
3475 tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
3478 $undo_tb.signal_connect('clicked') { perform_undo }
3479 $undo_mb.signal_connect('activate') { perform_undo }
3480 $redo_tb.signal_connect('clicked') { perform_redo }
3481 $redo_mb.signal_connect('activate') { perform_redo }
3483 one_click_explain_try = Proc.new {
3484 if !$config['one-click-explained']
3485 show_one_click_explanation(_("You have just clicked on a One-Click tool."))
3486 $config['one-click-explained'] = true
3490 $r90.signal_connect('toggled') {
3492 set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
3493 one_click_explain_try.call
3494 $r270.active = false
3495 $enhance.active = false
3496 $delete.active = false
3497 nothing.sensitive = true
3499 if !$r270.active? && !$enhance.active? && !$delete.active?
3500 set_mousecursor_normal
3501 nothing.sensitive = false
3503 nothing.sensitive = true
3507 $r270.signal_connect('toggled') {
3509 set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
3510 one_click_explain_try.call