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 > 10
155 $config['last-opens'] = $config['last-opens'][-10, 10]
158 ios = File.open($config_file, "w")
159 $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
160 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
161 $config.each_pair { |key, value|
162 elem = $xmldoc.root.add_element key
164 $config[key].each_pair { |subkey, subvalue|
165 subelem = elem.add_element subkey
166 subelem.add_text subvalue.to_s
168 elsif value.is_a? Array
169 elem.add_text value.join('~~~')
174 elem.add_text value.to_s
178 $xmldoc.write(ios, 0)
181 $tempfiles.each { |f|
186 def set_mousecursor(what, *widget)
187 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
188 if widget[0] && widget[0].window
189 widget[0].window.cursor = cursor
191 if $main_window && $main_window.window
192 $main_window.window.cursor = cursor
194 $current_cursor = what
196 def set_mousecursor_wait(*widget)
197 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
198 if Thread.current == Thread.main
199 Gtk.main_iteration while Gtk.events_pending?
202 def set_mousecursor_normal(*widget)
203 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
205 def push_mousecursor_wait(*widget)
206 if $current_cursor != Gdk::Cursor::WATCH
207 $save_cursor = $current_cursor
208 gtk_thread_protect { set_mousecursor_wait(*widget) }
211 def pop_mousecursor(*widget)
212 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
216 source = $xmldoc.root.attributes['source']
217 dest = $xmldoc.root.attributes['destination']
218 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
221 def full_src_dir_to_rel(path, source)
222 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
225 def build_full_dest_filename(filename)
226 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
229 def save_undo(name, closure, *params)
230 UndoHandler.save_undo(name, closure, [ *params ])
231 $undo_tb.sensitive = $undo_mb.sensitive = true
232 $redo_tb.sensitive = $redo_mb.sensitive = false
235 def view_element(filename, closures)
236 if entry2type(filename) == 'video'
237 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
243 w = Gtk::Window.new.set_title(filename)
245 msg 3, "filename: #{filename}"
246 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
247 #- typically this file won't exist in case of videos; try with the largest thumbnail around
248 if !File.exists?(dest_img)
249 if entry2type(filename) == 'video'
250 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
251 if not alternatives.empty?
252 dest_img = alternatives[-1]
255 push_mousecursor_wait
256 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
258 if !File.exists?(dest_img)
259 msg 2, _("Could not generate fullscreen thumbnail!")
264 evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(Gtk::Image.new(dest_img)).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
265 evt.signal_connect('button-press-event') { |this, event|
266 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
267 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
269 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
271 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
272 delete_item.signal_connect('activate') {
274 closures[:delete].call
277 menu.popup(nil, nil, event.button, event.time)
280 evt.signal_connect('button-release-event') { |this, event|
282 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
283 msg 3, "gesture delete: click-drag right button to the bottom"
285 closures[:delete].call
286 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
290 tooltips = Gtk::Tooltips.new
291 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
293 w.signal_connect('key-press-event') { |w,event|
294 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
296 closures[:delete].call
300 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
301 b.signal_connect('clicked') { w.destroy }
304 vb.pack_start(evt, false, false)
305 vb.pack_end(bottom, false, false)
308 w.signal_connect('delete-event') { w.destroy }
309 w.window_position = Gtk::Window::POS_CENTER
313 def scroll_upper(scrolledwindow, ypos_top)
314 newval = scrolledwindow.vadjustment.value -
315 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
316 if newval < scrolledwindow.vadjustment.lower
317 newval = scrolledwindow.vadjustment.lower
319 scrolledwindow.vadjustment.value = newval
322 def scroll_lower(scrolledwindow, ypos_bottom)
323 newval = scrolledwindow.vadjustment.value +
324 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
325 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
326 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
328 scrolledwindow.vadjustment.value = newval
331 def autoscroll_if_needed(scrolledwindow, image, textview)
332 #- autoscroll if cursor or image is not visible, if possible
333 if image && image.window || textview.window
334 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
335 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
336 current_miny_visible = scrolledwindow.vadjustment.value
337 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
338 if ypos_top < current_miny_visible
339 scroll_upper(scrolledwindow, ypos_top)
340 elsif ypos_bottom > current_maxy_visible
341 scroll_lower(scrolledwindow, ypos_bottom)
346 def create_editzone(scrolledwindow, pagenum, image)
347 frame = Gtk::Frame.new
348 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
349 frame.set_shadow_type(Gtk::SHADOW_IN)
350 textview.signal_connect('key-press-event') { |w, event|
351 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
352 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
353 scrolledwindow.signal_emit('key-press-event', event)
355 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
356 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
357 if event.keyval == Gdk::Keyval::GDK_Up
358 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
359 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
361 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
364 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
365 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
367 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
373 textview.signal_connect('focus-in-event') { |w, event|
374 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
378 candidate_undo_text = nil
379 textview.signal_connect('focus-in-event') { |w, event|
380 candidate_undo_text = textview.buffer.text
383 textview.signal_connect('key-release-event') { |w, event|
384 if candidate_undo_text && candidate_undo_text != textview.buffer.text
386 save_undo(_("text edit"),
388 save_text = textview.buffer.text
389 textview.buffer.text = text
391 $notebook.set_page(pagenum)
393 textview.buffer.text = save_text
395 $notebook.set_page(pagenum)
397 }, candidate_undo_text)
398 candidate_undo_text = nil
401 if event.state != 0 || ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
402 autoscroll_if_needed(scrolledwindow, image, textview)
407 return [ frame, textview ]
410 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
412 if !$modified_pixbufs[thumbnail_img]
413 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
414 elsif !$modified_pixbufs[thumbnail_img][:orig]
415 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
418 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
421 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
422 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
423 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
424 if pixbuf.height > desired_y
425 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
426 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
427 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
432 if $modified_pixbufs[thumbnail_img][:whitebalance]
433 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
436 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
439 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
442 #- update rotate attribute
443 xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
445 $modified_pixbufs[thumbnail_img] ||= {}
446 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
447 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
449 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
452 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
455 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
457 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
459 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
460 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
462 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
463 $notebook.set_page(0)
464 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
469 def color_swap(xmldir, attributes_prefix)
471 if xmldir.attributes["#{attributes_prefix}color-swap"]
472 xmldir.delete_attribute("#{attributes_prefix}color-swap")
474 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
478 def enhance(xmldir, attributes_prefix)
480 if xmldir.attributes["#{attributes_prefix}enhance"]
481 xmldir.delete_attribute("#{attributes_prefix}enhance")
483 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
487 def change_frame_offset(xmldir, attributes_prefix, value)
489 xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
492 def ask_new_frame_offset(xmldir, attributes_prefix)
494 value = xmldir.attributes["#{attributes_prefix}frame-offset"]
499 dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
501 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
502 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
503 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
507 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
508 from. There are approximately 25 frames per second in a video.
511 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
512 entry.signal_connect('key-press-event') { |w, event|
513 if event.keyval == Gdk::Keyval::GDK_Return
514 dialog.response(Gtk::Dialog::RESPONSE_OK)
516 elsif event.keyval == Gdk::Keyval::GDK_Escape
517 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
520 false #- propagate if needed
524 dialog.window_position = Gtk::Window::POS_MOUSE
527 dialog.run { |response|
530 if response == Gtk::Dialog::RESPONSE_OK
532 msg 3, "changing frame offset to #{newval}"
533 return { :old => value, :new => newval }
540 def change_pano_amount(xmldir, attributes_prefix, value)
543 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
545 xmldir.add_attribute("#{attributes_prefix}pano-amount", value)
549 def ask_new_pano_amount(xmldir, attributes_prefix)
551 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
556 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
558 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
559 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
560 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
564 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
565 of this panorama image compared to other regular images. For example, if the panorama
566 was taken out of four photos on one row, counting the necessary overlap, the width of
567 this panorama image should probably be roughly three times the width of regular images.
569 With this information, booh will be able to generate panorama thumbnails looking
573 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("none (not a panorama image)")))).
574 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
575 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
576 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
577 dialog.window_position = Gtk::Window::POS_MOUSE
580 spin.value = value.to_f
587 dialog.run { |response|
591 newval = spin.value.to_f
594 if response == Gtk::Dialog::RESPONSE_OK
596 msg 3, "changing panorama amount to #{newval}"
597 return { :old => value, :new => newval }
604 def change_whitebalance(xmlelem, attributes_prefix, value)
606 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
609 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
611 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
612 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
613 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
614 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
615 destfile = "#{thumbnail_img}-orig-whitebalance.jpg"
616 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
617 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
618 $modified_pixbufs[thumbnail_img] ||= {}
619 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
620 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
621 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
624 $modified_pixbufs[thumbnail_img] ||= {}
625 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
627 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
630 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
631 #- init $modified_pixbufs correctly
632 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
634 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
636 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
638 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
639 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
640 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
644 _("You can fix the <b>white balance</b> of the image, if your image is too blue
645 or too yellow because your camera didn't detect the light correctly. Drag the
646 slider below the image to the left for more blue, to the right for more yellow.
650 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
652 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
654 dialog.window_position = Gtk::Window::POS_MOUSE
658 timeout = Gtk.timeout_add(100) {
659 if hs.value != lastval
662 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
668 dialog.run { |response|
669 Gtk.timeout_remove(timeout)
670 if response == Gtk::Dialog::RESPONSE_OK
672 newval = hs.value.to_s
673 msg 3, "changing white balance to #{newval}"
675 return { :old => value, :new => newval }
678 $modified_pixbufs[thumbnail_img] ||= {}
679 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
680 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
688 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
689 system("rm -f '#{destfile}'")
690 #- type can be 'element' or 'subdir'
692 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
694 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
698 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
700 push_mousecursor_wait
701 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
704 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
710 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
711 distribute_multiple_call = Proc.new { |action, arg|
712 $selected_elements.each_key { |path|
713 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
715 if possible_actions[:can_multiple] && $selected_elements.length > 0
716 UndoHandler.begin_batch
717 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
718 UndoHandler.end_batch
720 closures[action].call(arg)
722 $selected_elements = {}
725 if optionals.include?('change_image')
726 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
727 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
728 changeimg.signal_connect('activate') { closures[:change].call }
729 menu.append(Gtk::SeparatorMenuItem.new)
731 if !possible_actions[:can_multiple] || $selected_elements.length == 0
734 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
735 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
736 view.signal_connect('activate') { closures[:view].call }
738 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
739 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
740 view.signal_connect('activate') { closures[:view].call }
741 menu.append(Gtk::SeparatorMenuItem.new)
744 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
745 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
746 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
747 exif.signal_connect('activate') { show_popup($main_window,
748 utf8(`identify -format "%[EXIF:*]" #{fullpath}`.sub(/MakerNote.*\n/, '')),
749 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
750 menu.append(Gtk::SeparatorMenuItem.new)
753 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
754 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
755 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
756 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
757 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
758 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
759 if !possible_actions[:can_multiple] || $selected_elements.length == 0
760 menu.append(Gtk::SeparatorMenuItem.new)
761 if !possible_actions[:forbid_left]
762 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
763 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
764 moveleft.signal_connect('activate') { closures[:move].call('left') }
765 if !possible_actions[:can_left]
766 moveleft.sensitive = false
769 if !possible_actions[:forbid_right]
770 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
771 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
772 moveright.signal_connect('activate') { closures[:move].call('right') }
773 if !possible_actions[:can_right]
774 moveright.sensitive = false
777 if optionals.include?('move_top')
778 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
779 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
780 movetop.signal_connect('activate') { closures[:move].call('top') }
781 if !possible_actions[:can_top]
782 movetop.sensitive = false
785 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
786 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
787 moveup.signal_connect('activate') { closures[:move].call('up') }
788 if !possible_actions[:can_up]
789 moveup.sensitive = false
791 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
792 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
793 movedown.signal_connect('activate') { closures[:move].call('down') }
794 if !possible_actions[:can_down]
795 movedown.sensitive = false
797 if optionals.include?('move_bottom')
798 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
799 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
800 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
801 if !possible_actions[:can_bottom]
802 movebottom.sensitive = false
807 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
808 menu.append(Gtk::SeparatorMenuItem.new)
809 menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
810 color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
811 color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
812 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
813 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
814 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
815 menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
816 frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
817 frame_offset.signal_connect('activate') {
818 if possible_actions[:can_multiple] && $selected_elements.length > 0
819 if values = ask_new_frame_offset(nil, '')
820 distribute_multiple_call.call(:frame_offset, values)
823 closures[:frame_offset].call
828 menu.append( Gtk::SeparatorMenuItem.new)
829 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
830 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
831 whitebalance.signal_connect('activate') {
832 if possible_actions[:can_multiple] && $selected_elements.length > 0
833 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
834 distribute_multiple_call.call(:whitebalance, values)
837 closures[:whitebalance].call
840 if !possible_actions[:can_multiple] || $selected_elements.length == 0
841 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
842 _("Enhance constrast"))))
844 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
846 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
847 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
848 if type == 'image' && possible_actions[:can_panorama]
849 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
850 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
851 panorama.signal_connect('activate') {
852 if possible_actions[:can_multiple] && $selected_elements.length > 0
853 if values = ask_new_pano_amount(nil, '')
854 distribute_multiple_call.call(:pano, values)
857 distribute_multiple_call.call(:pano)
861 if optionals.include?('delete')
862 menu.append( Gtk::SeparatorMenuItem.new)
863 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
864 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
865 if !possible_actions[:can_multiple] || $selected_elements.length == 0
866 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
867 paste_item.signal_connect('activate') { closures[:paste].call }
868 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
869 clear_item.signal_connect('activate') { $cuts = [] }
871 paste_item.sensitive = clear_item.sensitive = false
874 menu.append( Gtk::SeparatorMenuItem.new)
875 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
876 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
879 menu.popup(nil, nil, event.button, event.time)
882 def delete_current_subalbum
884 sel = $albums_tv.selection.selected_rows
885 $xmldir.elements.each { |e|
886 if e.name == 'image' || e.name == 'video'
887 e.add_attribute('deleted', 'true')
890 #- branch if we have a non deleted subalbum
891 if $xmldir.child_byname_notattr('dir', 'deleted')
892 $xmldir.delete_attribute('thumbnails-caption')
893 $xmldir.delete_attribute('thumbnails-captionfile')
895 $xmldir.add_attribute('deleted', 'true')
897 while moveup.parent.name == 'dir'
898 moveup = moveup.parent
899 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
900 moveup.add_attribute('deleted', 'true')
907 save_changes('forced')
908 populate_subalbums_treeview(false)
909 $albums_tv.selection.select_path(sel[0])
915 $current_path = nil #- prevent save_changes from being rerun again
916 sel = $albums_tv.selection.selected_rows
917 restore_one = proc { |xmldir|
918 xmldir.elements.each { |e|
919 if e.name == 'dir' && e.attributes['deleted']
922 e.delete_attribute('deleted')
925 restore_one.call($xmldir)
926 populate_subalbums_treeview(false)
927 $albums_tv.selection.select_path(sel[0])
930 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
933 frame1 = Gtk::Frame.new
934 fullpath = from_utf8("#{$current_path}/#{filename}")
936 my_gen_real_thumbnail = proc {
937 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
940 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
941 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
942 frame1.add(img = Gtk::Image.new)
943 my_gen_real_thumbnail.call
945 frame1.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
947 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
949 tooltips = Gtk::Tooltips.new
950 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
951 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
953 frame2, textview = create_editzone($autotable_sw, 1, img)
954 textview.buffer.text = caption
955 textview.set_justification(Gtk::Justification::CENTER)
957 vbox = Gtk::VBox.new(false, 5)
958 vbox.pack_start(evtbox, false, false)
959 vbox.pack_start(frame2, false, false)
960 autotable.append(vbox, filename)
962 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
963 $vbox2widgets[vbox] = { :textview => textview, :image => img }
965 #- to be able to find widgets by name
966 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
968 cleanup_all_thumbnails = Proc.new {
969 #- remove out of sync images
970 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
971 for sizeobj in $images_size
972 system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
977 rotate_and_cleanup = Proc.new { |angle|
978 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
979 cleanup_all_thumbnails.call
982 move = Proc.new { |direction|
983 do_method = "move_#{direction}"
984 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
986 done = autotable.method(do_method).call(vbox)
987 textview.grab_focus #- because if moving, focus is stolen
991 save_undo(_("move %s") % direction,
993 autotable.method(undo_method).call(vbox)
994 textview.grab_focus #- because if moving, focus is stolen
995 autoscroll_if_needed($autotable_sw, img, textview)
996 $notebook.set_page(1)
998 autotable.method(do_method).call(vbox)
999 textview.grab_focus #- because if moving, focus is stolen
1000 autoscroll_if_needed($autotable_sw, img, textview)
1001 $notebook.set_page(1)
1007 color_swap_and_cleanup = Proc.new {
1008 perform_color_swap_and_cleanup = Proc.new {
1009 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1010 my_gen_real_thumbnail.call
1013 cleanup_all_thumbnails.call
1014 perform_color_swap_and_cleanup.call
1016 save_undo(_("color swap"),
1018 perform_color_swap_and_cleanup.call
1020 autoscroll_if_needed($autotable_sw, img, textview)
1021 $notebook.set_page(1)
1023 perform_color_swap_and_cleanup.call
1025 autoscroll_if_needed($autotable_sw, img, textview)
1026 $notebook.set_page(1)
1031 change_frame_offset_and_cleanup_real = Proc.new { |values|
1032 perform_change_frame_offset_and_cleanup = Proc.new { |val|
1033 change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
1034 my_gen_real_thumbnail.call
1036 perform_change_frame_offset_and_cleanup.call(values[:new])
1038 save_undo(_("specify frame offset"),
1040 perform_change_frame_offset_and_cleanup.call(values[:old])
1042 autoscroll_if_needed($autotable_sw, img, textview)
1043 $notebook.set_page(1)
1045 perform_change_frame_offset_and_cleanup.call(values[:new])
1047 autoscroll_if_needed($autotable_sw, img, textview)
1048 $notebook.set_page(1)
1053 change_frame_offset_and_cleanup = Proc.new {
1054 if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
1055 change_frame_offset_and_cleanup_real.call(values)
1059 change_pano_amount_and_cleanup_real = Proc.new { |values|
1060 perform_change_pano_amount_and_cleanup = Proc.new { |val|
1061 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1063 perform_change_pano_amount_and_cleanup.call(values[:new])
1065 save_undo(_("change panorama amount"),
1067 perform_change_pano_amount_and_cleanup.call(values[:old])
1069 autoscroll_if_needed($autotable_sw, img, textview)
1070 $notebook.set_page(1)
1072 perform_change_pano_amount_and_cleanup.call(values[:new])
1074 autoscroll_if_needed($autotable_sw, img, textview)
1075 $notebook.set_page(1)
1080 change_pano_amount_and_cleanup = Proc.new {
1081 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1082 change_pano_amount_and_cleanup_real.call(values)
1086 whitebalance_and_cleanup_real = Proc.new { |values|
1087 perform_change_whitebalance_and_cleanup = Proc.new { |val|
1088 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1089 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1090 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1091 cleanup_all_thumbnails.call
1093 perform_change_whitebalance_and_cleanup.call(values[:new])
1095 save_undo(_("fix white balance"),
1097 perform_change_whitebalance_and_cleanup.call(values[:old])
1099 autoscroll_if_needed($autotable_sw, img, textview)
1100 $notebook.set_page(1)
1102 perform_change_whitebalance_and_cleanup.call(values[:new])
1104 autoscroll_if_needed($autotable_sw, img, textview)
1105 $notebook.set_page(1)
1110 whitebalance_and_cleanup = Proc.new {
1111 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1112 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1113 whitebalance_and_cleanup_real.call(values)
1117 enhance_and_cleanup = Proc.new {
1118 perform_enhance_and_cleanup = Proc.new {
1119 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1120 my_gen_real_thumbnail.call
1123 cleanup_all_thumbnails.call
1124 perform_enhance_and_cleanup.call
1126 save_undo(_("enhance"),
1128 perform_enhance_and_cleanup.call
1130 autoscroll_if_needed($autotable_sw, img, textview)
1131 $notebook.set_page(1)
1133 perform_enhance_and_cleanup.call
1135 autoscroll_if_needed($autotable_sw, img, textview)
1136 $notebook.set_page(1)
1141 delete = Proc.new { |isacut|
1142 if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed? This operation cannot be undone.")), { :okcancel => true })
1145 perform_delete = Proc.new {
1146 after = autotable.get_next_widget(vbox)
1148 after = autotable.get_previous_widget(vbox)
1150 if $config['deleteondisk'] && !isacut
1151 msg 3, "scheduling for delete: #{fullpath}"
1152 $todelete << fullpath
1154 autotable.remove(vbox)
1156 $vbox2widgets[after][:textview].grab_focus
1157 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1161 previous_pos = autotable.get_current_number(vbox)
1165 delete_current_subalbum
1167 save_undo(_("delete"),
1169 autotable.reinsert(pos, vbox, filename)
1170 $notebook.set_page(1)
1171 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1173 msg 3, "removing deletion schedule of: #{fullpath}"
1174 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1177 $notebook.set_page(1)
1186 $cuts << { :vbox => vbox, :filename => filename }
1187 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1192 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1195 autotable.queue_draws << proc {
1196 $vbox2widgets[last[:vbox]][:textview].grab_focus
1197 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1199 save_undo(_("paste"),
1201 cuts.each { |elem| autotable.remove(elem[:vbox]) }
1202 $notebook.set_page(1)
1205 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1207 $notebook.set_page(1)
1210 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1215 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1216 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1217 :whitebalance => whitebalance_and_cleanup_real, :pano => change_pano_amount_and_cleanup_real }
1219 textview.signal_connect('key-press-event') { |w, event|
1222 x, y = autotable.get_current_pos(vbox)
1223 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1224 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1225 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1226 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1228 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1229 $vbox2widgets[widget_up][:textview].grab_focus
1236 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1238 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1239 $vbox2widgets[widget_down][:textview].grab_focus
1246 if event.keyval == Gdk::Keyval::GDK_Left
1249 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1256 rotate_and_cleanup.call(-90)
1259 if event.keyval == Gdk::Keyval::GDK_Right
1260 next_ = autotable.get_next_widget(vbox)
1261 if next_ && autotable.get_current_pos(next_)[0] > x
1263 $vbox2widgets[next_][:textview].grab_focus
1270 rotate_and_cleanup.call(90)
1273 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1276 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1277 view_element(filename, { :delete => delete })
1280 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1283 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1287 !propagate #- propagate if needed
1290 $ignore_next_release = false
1291 evtbox.signal_connect('button-press-event') { |w, event|
1292 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1293 if event.state & Gdk::Window::BUTTON3_MASK != 0
1294 #- gesture redo: hold right mouse button then click left mouse button
1295 $config['nogestures'] or perform_redo
1296 $ignore_next_release = true
1298 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1300 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1302 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1303 elsif $enhance.active?
1304 enhance_and_cleanup.call
1305 elsif $delete.active?
1309 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1312 $button1_pressed_autotable = true
1313 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1314 if event.state & Gdk::Window::BUTTON1_MASK != 0
1315 #- gesture undo: hold left mouse button then click right mouse button
1316 $config['nogestures'] or perform_undo
1317 $ignore_next_release = true
1319 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1320 view_element(filename, { :delete => delete })
1325 evtbox.signal_connect('button-release-event') { |w, event|
1326 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1327 if !$ignore_next_release
1328 x, y = autotable.get_current_pos(vbox)
1329 next_ = autotable.get_next_widget(vbox)
1330 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1331 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1332 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1333 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1334 :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1335 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1336 :pano => change_pano_amount_and_cleanup })
1338 $ignore_next_release = false
1339 $gesture_press = nil
1344 #- handle reordering with drag and drop
1345 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1346 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1347 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1348 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1351 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1353 #- mouse gesture first (dnd disables button-release-event)
1354 if $gesture_press && $gesture_press[:filename] == filename
1355 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1356 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1357 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1358 rotate_and_cleanup.call(angle)
1359 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1361 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1362 msg 3, "gesture delete: click-drag right button to the bottom"
1364 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1369 ctxt.targets.each { |target|
1370 if target.name == 'reorder-elements'
1371 move_dnd = Proc.new { |from,to|
1374 autotable.move(from, to)
1375 save_undo(_("reorder"),
1376 Proc.new { |from, to|
1378 autotable.move(to - 1, from)
1380 autotable.move(to, from + 1)
1382 $notebook.set_page(1)
1384 autotable.move(from, to)
1385 $notebook.set_page(1)
1390 if $multiple_dnd.size == 0
1391 move_dnd.call(selection_data.data.to_i,
1392 autotable.get_current_number(vbox))
1394 UndoHandler.begin_batch
1395 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1397 #- need to update current position between each call
1398 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1399 autotable.get_current_number(vbox))
1401 UndoHandler.end_batch
1412 def create_auto_table
1414 $autotable = Gtk::AutoTable.new(5)
1416 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1417 thumbnails_vb = Gtk::VBox.new(false, 5)
1419 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1420 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1421 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1422 thumbnails_vb.add($autotable)
1424 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1425 $autotable_sw.add_with_viewport(thumbnails_vb)
1427 #- follows stuff for handling multiple elements selection
1428 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1430 update_selected = Proc.new {
1431 $autotable.current_order.each { |path|
1432 w = $name2widgets[path][:evtbox].window
1433 xm = w.position[0] + w.size[0]/2
1434 ym = w.position[1] + w.size[1]/2
1435 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1436 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1437 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1438 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1441 if $selected_elements[path] && ! $selected_elements[path][:keep]
1442 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))
1443 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1444 $selected_elements.delete(path)
1449 $autotable.signal_connect('realize') { |w,e|
1450 gc = Gdk::GC.new($autotable.window)
1451 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1452 gc.function = Gdk::GC::INVERT
1453 #- autoscroll handling for DND and multiple selections
1454 Gtk.timeout_add(100) {
1455 w, x, y, mask = $autotable.window.pointer
1456 if mask & Gdk::Window::BUTTON1_MASK != 0
1457 if y < $autotable_sw.vadjustment.value
1459 $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]])
1461 if $button1_pressed_autotable || press_x
1462 scroll_upper($autotable_sw, y)
1465 w, pos_x, pos_y = $autotable.window.pointer
1466 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1467 update_selected.call
1470 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1472 $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]])
1474 if $button1_pressed_autotable || press_x
1475 scroll_lower($autotable_sw, y)
1478 w, pos_x, pos_y = $autotable.window.pointer
1479 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1480 update_selected.call
1488 $autotable.signal_connect('button-press-event') { |w,e|
1490 if !$button1_pressed_autotable
1493 if e.state & Gdk::Window::SHIFT_MASK == 0
1494 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1495 $selected_elements = {}
1496 $statusbar.push(0, utf8(_("Nothing selected.")))
1498 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1500 set_mousecursor(Gdk::Cursor::TCROSS)
1504 $autotable.signal_connect('button-release-event') { |w,e|
1506 if $button1_pressed_autotable
1507 #- unselect all only now
1508 $multiple_dnd = $selected_elements.keys
1509 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1510 $selected_elements = {}
1511 $button1_pressed_autotable = false
1514 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1515 if $selected_elements.length > 0
1516 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1519 press_x = press_y = pos_x = pos_y = nil
1520 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1524 $autotable.signal_connect('motion-notify-event') { |w,e|
1527 $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]])
1531 $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]])
1532 update_selected.call
1538 def create_subalbums_page
1540 subalbums_hb = Gtk::HBox.new
1541 $subalbums_vb = Gtk::VBox.new(false, 5)
1542 subalbums_hb.pack_start($subalbums_vb, false, false)
1543 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1544 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1545 $subalbums_sw.add_with_viewport(subalbums_hb)
1548 def save_current_file
1552 ios = File.open($filename, "w")
1553 $xmldoc.write(ios, 0)
1558 def save_current_file_user
1559 save_tempfilename = $filename
1560 $filename = $orig_filename
1563 $generated_outofline = false
1564 $filename = save_tempfilename
1566 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1567 $todelete.each { |f|
1568 system("rm -f #{f}")
1572 def mark_document_as_dirty
1573 $xmldoc.elements.each('//dir') { |elem|
1574 elem.delete_attribute('already-generated')
1578 #- ret: true => ok false => cancel
1579 def ask_save_modifications(msg1, msg2, *options)
1581 options = options.size > 0 ? options[0] : {}
1583 if options[:disallow_cancel]
1584 dialog = Gtk::Dialog.new(msg1,
1586 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1587 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1588 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1590 dialog = Gtk::Dialog.new(msg1,
1592 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1593 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1594 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1595 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1597 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1598 dialog.vbox.add(Gtk::Label.new(msg2))
1599 dialog.window_position = Gtk::Window::POS_CENTER
1602 dialog.run { |response|
1604 if response == Gtk::Dialog::RESPONSE_YES
1605 save_current_file_user
1607 #- if we have generated an album but won't save modifications, we must remove
1608 #- already-generated markers in original file
1609 if $generated_outofline
1611 $xmldoc = REXML::Document.new File.new($orig_filename)
1612 mark_document_as_dirty
1613 ios = File.open($orig_filename, "w")
1614 $xmldoc.write(ios, 0)
1617 puts "exception: #{$!}"
1621 if response == Gtk::Dialog::RESPONSE_CANCEL
1624 $todelete = [] #- unconditionally clear the list of images/videos to delete
1630 def try_quit(*options)
1631 if ask_save_modifications(utf8(_("Save before quitting?")),
1632 utf8(_("Do you want to save your changes before quitting?")),
1638 def show_popup(parent, msg, *options)
1639 dialog = Gtk::Dialog.new
1640 if options[0] && options[0][:title]
1641 dialog.title = options[0][:title]
1643 dialog.title = utf8(_("Booh message"))
1645 lbl = Gtk::Label.new
1646 if options[0] && options[0][:nomarkup]
1651 if options[0] && options[0][:centered]
1652 lbl.set_justify(Gtk::Justification::CENTER)
1654 if options[0] && options[0][:selectable]
1655 lbl.selectable = true
1657 if options[0] && options[0][:topwidget]
1658 dialog.vbox.add(options[0][:topwidget])
1660 if options[0] && options[0][:scrolled]
1661 sw = Gtk::ScrolledWindow.new(nil, nil)
1662 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1663 sw.add_with_viewport(lbl)
1665 dialog.set_default_size(400, 500)
1667 dialog.vbox.add(lbl)
1668 dialog.set_default_size(200, 120)
1670 if options[0] && options[0][:okcancel]
1671 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1673 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1675 if options[0] && options[0][:pos_centered]
1676 dialog.window_position = Gtk::Window::POS_CENTER
1678 dialog.window_position = Gtk::Window::POS_MOUSE
1681 if options[0] && options[0][:linkurl]
1682 linkbut = Gtk::Button.new('')
1683 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1684 linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1685 linkbut.relief = Gtk::RELIEF_NONE
1686 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1687 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1688 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1693 if !options[0] || !options[0][:not_transient]
1694 dialog.transient_for = parent
1695 dialog.run { |response|
1697 if options[0] && options[0][:okcancel]
1698 return response == Gtk::Dialog::RESPONSE_OK
1702 dialog.signal_connect('response') { dialog.destroy }
1706 def backend_wait_message(parent, msg, infopipe_path, mode)
1708 w.set_transient_for(parent)
1711 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1712 vb.pack_start(Gtk::Label.new(msg), false, false)
1714 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1715 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1716 if mode != 'one dir scan'
1717 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1719 if mode == 'web-album'
1720 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1721 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1723 vb.pack_start(Gtk::HSeparator.new, false, false)
1725 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1726 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1727 vb.pack_end(bottom, false, false)
1729 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1730 refresh_thread = Thread.new {
1731 directories_counter = 0
1732 while line = infopipe.gets
1733 if line =~ /^directories: (\d+), sizes: (\d+)/
1734 directories = $1.to_f + 1
1736 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1737 elements = $3.to_f + 1
1738 if mode == 'web-album'
1742 gtk_thread_protect { pb1_1.fraction = 0 }
1743 if mode != 'one dir scan'
1744 newtext = utf8(full_src_dir_to_rel($1, $2))
1745 newtext = '/' if newtext == ''
1746 gtk_thread_protect { pb1_2.text = newtext }
1747 directories_counter += 1
1748 gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1750 elsif line =~ /^processing element$/
1751 element_counter += 1
1752 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1753 elsif line =~ /^processing size$/
1754 element_counter += 1
1755 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1756 elsif line =~ /^finished processing sizes$/
1757 gtk_thread_protect { pb1_1.fraction = 1 }
1758 elsif line =~ /^creating index.html$/
1759 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1760 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1761 directories_counter = 0
1762 elsif line =~ /^index.html: (.+)\|(.+)/
1763 newtext = utf8(full_src_dir_to_rel($1, $2))
1764 newtext = '/' if newtext == ''
1765 gtk_thread_protect { pb2.text = newtext }
1766 directories_counter += 1
1767 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1768 elsif line =~ /^die: (.*)$/
1775 w.signal_connect('delete-event') { w.destroy }
1776 w.signal_connect('destroy') {
1777 Thread.kill(refresh_thread)
1778 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1781 system("rm -f #{infopipe_path}")
1784 w.window_position = Gtk::Window::POS_CENTER
1790 def call_backend(cmd, waitmsg, mode, params)
1791 pipe = Tempfile.new("boohpipe")
1793 system("mkfifo #{pipe.path}")
1794 cmd += " --info-pipe #{pipe.path}"
1795 button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
1800 id, exitstatus = Process.waitpid2(pid)
1801 gtk_thread_protect { w8.destroy }
1803 if params[:successmsg]
1804 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
1806 if params[:closure_after]
1807 gtk_thread_protect(¶ms[:closure_after])
1809 elsif exitstatus == 15
1810 #- say nothing, user aborted
1812 gtk_thread_protect { show_popup($main_window,
1813 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
1819 button.signal_connect('clicked') {
1820 Process.kill('SIGTERM', pid)
1824 def save_changes(*forced)
1825 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1829 $xmldir.delete_attribute('already-generated')
1831 propagate_children = Proc.new { |xmldir|
1832 if xmldir.attributes['subdirs-caption']
1833 xmldir.delete_attribute('already-generated')
1835 xmldir.elements.each('dir') { |element|
1836 propagate_children.call(element)
1840 if $xmldir.child_byname_notattr('dir', 'deleted')
1841 new_title = $subalbums_title.buffer.text
1842 if new_title != $xmldir.attributes['subdirs-caption']
1843 parent = $xmldir.parent
1844 if parent.name == 'dir'
1845 parent.delete_attribute('already-generated')
1847 propagate_children.call($xmldir)
1849 $xmldir.add_attribute('subdirs-caption', new_title)
1850 $xmldir.elements.each('dir') { |element|
1851 if !element.attributes['deleted']
1852 path = element.attributes['path']
1853 newtext = $subalbums_edits[path][:editzone].buffer.text
1854 if element.attributes['subdirs-caption']
1855 if element.attributes['subdirs-caption'] != newtext
1856 propagate_children.call(element)
1858 element.add_attribute('subdirs-caption', newtext)
1859 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1861 if element.attributes['thumbnails-caption'] != newtext
1862 element.delete_attribute('already-generated')
1864 element.add_attribute('thumbnails-caption', newtext)
1865 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1871 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
1872 if $xmldir.attributes['thumbnails-caption']
1873 path = $xmldir.attributes['path']
1874 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1876 elsif $xmldir.attributes['thumbnails-caption']
1877 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
1880 #- remove and reinsert elements to reflect new ordering
1883 $xmldir.elements.each { |element|
1884 if element.name == 'image' || element.name == 'video'
1885 saves[element.attributes['filename']] = element.remove
1889 $autotable.current_order.each { |path|
1890 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1891 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1894 saves.each_key { |path|
1895 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1896 chld.add_attribute('deleted', 'true')
1900 def sort_by_exif_date
1904 $xmldir.elements.each { |element|
1905 if element.name == 'image' || element.name == 'video'
1906 current_order << element.attributes['filename']
1910 #- look for EXIF dates
1912 w.set_transient_for($main_window)
1914 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1915 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
1916 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
1917 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1918 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1919 vb.pack_end(bottom, false, false)
1921 w.signal_connect('delete-event') { w.destroy }
1922 w.window_position = Gtk::Window::POS_CENTER
1926 b.signal_connect('clicked') { aborted = true }
1929 current_order.each { |f|
1931 if entry2type(f) == 'image'
1933 pb.fraction = i.to_f / current_order.size
1934 Gtk.main_iteration while Gtk.events_pending?
1935 date_time = `identify -format "%[EXIF:DateTime]" '#{from_utf8($current_path + "/" + f)}'`.chomp
1936 if $? == 0 && date_time != ''
1937 dates[f] = date_time
1950 $xmldir.elements.each { |element|
1951 if element.name == 'image' || element.name == 'video'
1952 saves[element.attributes['filename']] = element.remove
1956 #- find a good fallback for all entries without a date (still next to the item they were next to)
1957 neworder = dates.keys.sort { |a,b| dates[a] <=> dates[b] }
1958 for i in 0 .. current_order.size - 1
1959 if ! neworder.include?(current_order[i])
1961 while j > 0 && ! neworder.include?(current_order[j])
1964 neworder[(neworder.index(current_order[j]) || -1 ) + 1, 0] = current_order[i]
1968 $xmldir.add_element(saves[f].name, saves[f].attributes)
1971 #- let the auto-table reflect new ordering
1975 def remove_all_captions
1978 $autotable.current_order.each { |path|
1979 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
1980 $name2widgets[File.basename(path)][:textview].buffer.text = ''
1982 save_undo(_("remove all captions"),
1984 texts.each_key { |key|
1985 $name2widgets[key][:textview].buffer.text = texts[key]
1987 $notebook.set_page(1)
1989 texts.each_key { |key|
1990 $name2widgets[key][:textview].buffer.text = ''
1992 $notebook.set_page(1)
1998 $selected_elements.each_key { |path|
1999 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2005 $selected_elements = {}
2009 $undo_tb.sensitive = $undo_mb.sensitive = false
2010 $redo_tb.sensitive = $redo_mb.sensitive = false
2016 $subalbums_vb.children.each { |chld|
2017 $subalbums_vb.remove(chld)
2019 $subalbums = Gtk::Table.new(0, 0, true)
2020 current_y_sub_albums = 0
2022 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2023 $subalbums_edits = {}
2024 subalbums_counter = 0
2025 subalbums_edits_bypos = {}
2027 add_subalbum = Proc.new { |xmldir, counter|
2028 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2029 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2030 if xmldir == $xmldir
2031 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2032 caption = xmldir.attributes['thumbnails-caption']
2033 captionfile, dummy = find_subalbum_caption_info(xmldir)
2034 infotype = 'thumbnails'
2036 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2037 captionfile, caption = find_subalbum_caption_info(xmldir)
2038 infotype = find_subalbum_info_type(xmldir)
2040 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2041 hbox = Gtk::HBox.new
2042 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2044 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2047 my_gen_real_thumbnail = proc {
2048 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2051 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2052 f.add(img = Gtk::Image.new)
2053 my_gen_real_thumbnail.call
2055 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2057 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2058 $subalbums.attach(hbox,
2059 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2061 frame, textview = create_editzone($subalbums_sw, 0, img)
2062 textview.buffer.text = caption
2063 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2064 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2066 change_image = Proc.new {
2067 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2069 Gtk::FileChooser::ACTION_OPEN,
2071 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2072 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2073 fc.transient_for = $main_window
2074 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))
2075 f.add(preview_img = Gtk::Image.new)
2077 fc.signal_connect('update-preview') { |w|
2079 if fc.preview_filename
2080 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2081 fc.preview_widget_active = true
2083 rescue Gdk::PixbufError
2084 fc.preview_widget_active = false
2087 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2089 old_file = captionfile
2090 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2091 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2092 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2093 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2095 new_file = fc.filename
2096 msg 3, "new captionfile is: #{fc.filename}"
2097 perform_changefile = Proc.new {
2098 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2099 $modified_pixbufs.delete(thumbnail_file)
2100 xmldir.delete_attribute("#{infotype}-rotate")
2101 xmldir.delete_attribute("#{infotype}-color-swap")
2102 xmldir.delete_attribute("#{infotype}-enhance")
2103 xmldir.delete_attribute("#{infotype}-frame-offset")
2104 my_gen_real_thumbnail.call
2106 perform_changefile.call
2108 save_undo(_("change caption file for sub-album"),
2110 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2111 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2112 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2113 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2114 xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2115 my_gen_real_thumbnail.call
2116 $notebook.set_page(0)
2118 perform_changefile.call
2119 $notebook.set_page(0)
2126 rotate_and_cleanup = Proc.new { |angle|
2127 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2128 system("rm -f '#{thumbnail_file}'")
2131 move = Proc.new { |direction|
2134 save_changes('forced')
2135 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2136 if direction == 'up'
2137 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2138 subalbums_edits_bypos[oldpos - 1][:position] += 1
2140 if direction == 'down'
2141 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2142 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2144 if direction == 'top'
2145 for i in 1 .. oldpos - 1
2146 subalbums_edits_bypos[i][:position] += 1
2148 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2150 if direction == 'bottom'
2151 for i in oldpos + 1 .. subalbums_counter
2152 subalbums_edits_bypos[i][:position] -= 1
2154 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2158 $xmldir.elements.each('dir') { |element|
2159 if (!element.attributes['deleted'])
2160 elems << [ element.attributes['path'], element.remove ]
2163 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2164 each { |e| $xmldir.add_element(e[1]) }
2165 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2166 $xmldir.elements.each('descendant::dir') { |elem|
2167 elem.delete_attribute('already-generated')
2170 sel = $albums_tv.selection.selected_rows
2172 populate_subalbums_treeview(false)
2173 $albums_tv.selection.select_path(sel[0])
2176 color_swap_and_cleanup = Proc.new {
2177 perform_color_swap_and_cleanup = Proc.new {
2178 color_swap(xmldir, "#{infotype}-")
2179 my_gen_real_thumbnail.call
2181 perform_color_swap_and_cleanup.call
2183 save_undo(_("color swap"),
2185 perform_color_swap_and_cleanup.call
2186 $notebook.set_page(0)
2188 perform_color_swap_and_cleanup.call
2189 $notebook.set_page(0)
2194 change_frame_offset_and_cleanup = Proc.new {
2195 if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2196 perform_change_frame_offset_and_cleanup = Proc.new { |val|
2197 change_frame_offset(xmldir, "#{infotype}-", val)
2198 my_gen_real_thumbnail.call
2200 perform_change_frame_offset_and_cleanup.call(values[:new])
2202 save_undo(_("specify frame offset"),
2204 perform_change_frame_offset_and_cleanup.call(values[:old])
2205 $notebook.set_page(0)
2207 perform_change_frame_offset_and_cleanup.call(values[:new])
2208 $notebook.set_page(0)
2214 whitebalance_and_cleanup = Proc.new {
2215 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2216 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2217 perform_change_whitebalance_and_cleanup = Proc.new { |val|
2218 change_whitebalance(xmldir, "#{infotype}-", val)
2219 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2220 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2221 system("rm -f '#{thumbnail_file}'")
2223 perform_change_whitebalance_and_cleanup.call(values[:new])
2225 save_undo(_("fix white balance"),
2227 perform_change_whitebalance_and_cleanup.call(values[:old])
2228 $notebook.set_page(0)
2230 perform_change_whitebalance_and_cleanup.call(values[:new])
2231 $notebook.set_page(0)
2237 enhance_and_cleanup = Proc.new {
2238 perform_enhance_and_cleanup = Proc.new {
2239 enhance(xmldir, "#{infotype}-")
2240 my_gen_real_thumbnail.call
2243 perform_enhance_and_cleanup.call
2245 save_undo(_("enhance"),
2247 perform_enhance_and_cleanup.call
2248 $notebook.set_page(0)
2250 perform_enhance_and_cleanup.call
2251 $notebook.set_page(0)
2256 evtbox.signal_connect('button-press-event') { |w, event|
2257 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2259 rotate_and_cleanup.call(90)
2261 rotate_and_cleanup.call(-90)
2262 elsif $enhance.active?
2263 enhance_and_cleanup.call
2266 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2267 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2268 { :forbid_left => true, :forbid_right => true,
2269 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2270 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2271 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2272 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
2274 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2279 evtbox.signal_connect('button-press-event') { |w, event|
2280 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2284 evtbox.signal_connect('button-release-event') { |w, event|
2285 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2286 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2287 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2288 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2289 msg 3, "gesture rotate: #{angle}"
2290 rotate_and_cleanup.call(angle)
2293 $gesture_press = nil
2296 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2297 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2298 current_y_sub_albums += 1
2301 if $xmldir.child_byname_notattr('dir', 'deleted')
2303 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2304 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2305 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2306 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2307 #- this album image/caption
2308 if $xmldir.attributes['thumbnails-caption']
2309 add_subalbum.call($xmldir, 0)
2312 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2313 $xmldir.elements.each { |element|
2314 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2315 #- element (image or video) of this album
2316 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2317 msg 3, "dest_img: #{dest_img}"
2318 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2319 total[element.name] += 1
2321 if element.name == 'dir' && !element.attributes['deleted']
2322 #- sub-album image/caption
2323 add_subalbum.call(element, subalbums_counter += 1)
2324 total[element.name] += 1
2327 $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2328 total['image'], total['video'], total['dir'] ]))
2329 $subalbums_vb.add($subalbums)
2330 $subalbums_vb.show_all
2332 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2333 $notebook.get_tab_label($autotable_sw).sensitive = false
2334 $notebook.set_page(0)
2335 $thumbnails_title.buffer.text = ''
2337 $notebook.get_tab_label($autotable_sw).sensitive = true
2338 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2341 if !$xmldir.child_byname_notattr('dir', 'deleted')
2342 $notebook.get_tab_label($subalbums_sw).sensitive = false
2343 $notebook.set_page(1)
2345 $notebook.get_tab_label($subalbums_sw).sensitive = true
2349 def pixbuf_or_nil(filename)
2351 return Gdk::Pixbuf.new(filename)
2357 def theme_choose(current)
2358 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2360 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2361 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2362 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2364 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2365 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2366 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2367 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2368 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2369 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2370 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2371 treeview.signal_connect('button-press-event') { |w, event|
2372 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2373 dialog.response(Gtk::Dialog::RESPONSE_OK)
2377 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2379 `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2382 iter[0] = File.basename(dir)
2383 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2384 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2385 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2386 if File.basename(dir) == current
2387 treeview.selection.select_iter(iter)
2391 dialog.set_default_size(700, 400)
2392 dialog.vbox.show_all
2393 dialog.run { |response|
2394 iter = treeview.selection.selected
2396 if response == Gtk::Dialog::RESPONSE_OK && iter
2397 return model.get_value(iter, 0)
2403 def show_password_protections
2404 examine_dir_elem = Proc.new { |parent_iter, xmldir, already_protected|
2405 child_iter = $albums_iters[xmldir.attributes['path']]
2406 if xmldir.attributes['password-protect']
2407 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2408 already_protected = true
2409 elsif already_protected
2410 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2412 pix = pix.saturate_and_pixelate(1, true)
2418 xmldir.elements.each('dir') { |elem|
2419 if !elem.attributes['deleted']
2420 examine_dir_elem.call(child_iter, elem, already_protected)
2424 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2427 def populate_subalbums_treeview(select_first)
2431 $subalbums_vb.children.each { |chld|
2432 $subalbums_vb.remove(chld)
2435 source = $xmldoc.root.attributes['source']
2436 msg 3, "source: #{source}"
2438 xmldir = $xmldoc.elements['//dir']
2439 if !xmldir || xmldir.attributes['path'] != source
2440 msg 1, _("Corrupted booh file...")
2444 append_dir_elem = Proc.new { |parent_iter, xmldir|
2445 child_iter = $albums_ts.append(parent_iter)
2446 child_iter[0] = File.basename(xmldir.attributes['path'])
2447 child_iter[1] = xmldir.attributes['path']
2448 $albums_iters[xmldir.attributes['path']] = child_iter
2449 msg 3, "puttin location: #{xmldir.attributes['path']}"
2450 xmldir.elements.each('dir') { |elem|
2451 if !elem.attributes['deleted']
2452 append_dir_elem.call(child_iter, elem)
2456 append_dir_elem.call(nil, xmldir)
2457 show_password_protections
2459 $albums_tv.expand_all
2461 $albums_tv.selection.select_iter($albums_ts.iter_first)
2465 def open_file(filename)
2469 $current_path = nil #- invalidate
2470 $modified_pixbufs = {}
2473 $subalbums_vb.children.each { |chld|
2474 $subalbums_vb.remove(chld)
2477 if !File.exists?(filename)
2478 return utf8(_("File not found."))
2482 $xmldoc = REXML::Document.new File.new(filename)
2487 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2488 if entry2type(filename).nil?
2489 return utf8(_("Not a booh file!"))
2491 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."))
2495 if !source = $xmldoc.root.attributes['source']
2496 return utf8(_("Corrupted booh file..."))
2499 if !dest = $xmldoc.root.attributes['destination']
2500 return utf8(_("Corrupted booh file..."))
2503 if !theme = $xmldoc.root.attributes['theme']
2504 return utf8(_("Corrupted booh file..."))
2507 if $xmldoc.root.attributes['version'] < '0.8.4'
2508 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2509 mark_document_as_dirty
2510 if $xmldoc.root.attributes['version'] < '0.8.4'
2511 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2512 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2513 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2514 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2515 if old_dest_dir != new_dest_dir
2516 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2518 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2519 xmldir.elements.each { |element|
2520 if %w(image video).include?(element.name) && !element.attributes['deleted']
2521 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2522 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2523 Dir[old_name + '*'].each { |file|
2524 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2525 file != new_file and sys("mv '#{file}' '#{new_file}'")
2528 if element.name == 'dir' && !element.attributes['deleted']
2529 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2530 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2531 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2535 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2539 $xmldoc.root.add_attribute('version', $VERSION)
2542 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2543 optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2544 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2546 $filename = filename
2547 select_theme(theme, limit_sizes, optimizefor32, nperrow)
2548 $default_size['thumbnails'] =~ /(.*)x(.*)/
2549 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2550 $albums_thumbnail_size =~ /(.*)x(.*)/
2551 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2553 populate_subalbums_treeview(true)
2555 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
2559 def open_file_user(filename)
2560 result = open_file(filename)
2562 $config['last-opens'] ||= []
2563 if $config['last-opens'][-1] != utf8(filename)
2564 $config['last-opens'] << utf8(filename)
2566 $orig_filename = $filename
2567 tmp = Tempfile.new("boohtemp")
2570 ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2572 $tempfiles << $filename << "#{$filename}.backup"
2574 $orig_filename = nil
2580 if !ask_save_modifications(utf8(_("Save this album?")),
2581 utf8(_("Do you want to save the changes to this album?")),
2582 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2585 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2587 Gtk::FileChooser::ACTION_OPEN,
2589 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2590 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2591 fc.set_current_folder(File.expand_path("~/.booh"))
2592 fc.transient_for = $main_window
2595 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2596 push_mousecursor_wait(fc)
2597 msg = open_file_user(fc.filename)
2613 cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2618 def additional_booh_options
2621 options += "--mproc #{$config['mproc'].to_i} "
2623 if $config['emptycomments']
2624 options += "--empty-comments "
2630 if !ask_save_modifications(utf8(_("Save this album?")),
2631 utf8(_("Do you want to save the changes to this album?")),
2632 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2635 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2637 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2638 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2639 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2641 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2642 tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2643 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2644 tbl.attach(src = Gtk::Entry.new,
2645 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2646 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2647 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2648 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2649 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2650 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2651 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2652 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2653 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2654 tbl.attach(dest = Gtk::Entry.new,
2655 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2656 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2657 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2658 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2659 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2660 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2661 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2662 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2663 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2665 tooltips = Gtk::Tooltips.new
2666 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2667 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2668 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2669 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2670 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2671 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2672 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)
2673 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2674 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2675 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2676 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2677 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)
2679 src_nb_calculated_for = ''
2681 process_src_nb = Proc.new {
2682 if src.text != src_nb_calculated_for
2683 src_nb_calculated_for = src.text
2685 Thread.kill(src_nb_thread)
2688 if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2689 if File.readable?(from_utf8(src_nb_calculated_for))
2690 src_nb_thread = Thread.new {
2691 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2692 total = { 'image' => 0, 'video' => 0, nil => 0 }
2693 `find '#{from_utf8(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2694 if File.basename(dir) =~ /^\./
2698 Dir.entries(dir.chomp).each { |file|
2699 total[entry2type(file)] += 1
2701 rescue Errno::EACCES, Errno::ENOENT
2705 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2709 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2712 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2717 timeout_src_nb = Gtk.timeout_add(100) {
2721 src_browse.signal_connect('clicked') {
2722 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2724 Gtk::FileChooser::ACTION_SELECT_FOLDER,
2726 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2727 fc.transient_for = $main_window
2728 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2729 src.text = utf8(fc.filename)
2731 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2736 dest_browse.signal_connect('clicked') {
2737 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2739 Gtk::FileChooser::ACTION_CREATE_FOLDER,
2741 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2742 fc.transient_for = $main_window
2743 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2744 dest.text = utf8(fc.filename)
2749 conf_browse.signal_connect('clicked') {
2750 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2752 Gtk::FileChooser::ACTION_SAVE,
2754 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2755 fc.transient_for = $main_window
2756 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2757 fc.set_current_folder(File.expand_path("~/.booh"))
2758 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2759 conf.text = utf8(fc.filename)
2766 recreate_theme_config = proc {
2767 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2769 select_theme(theme_button.label, 'all', optimize432.active?, nil)
2770 $images_size.each { |s|
2771 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2775 tooltips.set_tip(cb, utf8(s['description']), nil)
2776 theme_sizes << { :widget => cb, :value => s['name'] }
2778 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2779 tooltips = Gtk::Tooltips.new
2780 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2781 theme_sizes << { :widget => cb, :value => 'original' }
2784 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2787 $allowed_N_values.each { |n|
2789 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2791 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2793 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2797 nperrows << { :widget => rb, :value => n }
2799 nperrowradios.show_all
2801 recreate_theme_config.call
2803 theme_button.signal_connect('clicked') {
2804 if newtheme = theme_choose(theme_button.label)
2805 theme_button.label = newtheme
2806 recreate_theme_config.call
2810 dialog.vbox.add(frame1)
2811 dialog.vbox.add(frame2)
2812 dialog.window_position = Gtk::Window::POS_MOUSE
2818 dialog.run { |response|
2819 if response == Gtk::Dialog::RESPONSE_OK
2820 srcdir = from_utf8(src.text)
2821 destdir = from_utf8(dest.text)
2822 if !File.directory?(srcdir)
2823 show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2825 elsif conf.text == ''
2826 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2828 elsif File.directory?(from_utf8(conf.text))
2829 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
2831 elsif destdir != make_dest_filename(destdir)
2832 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2834 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
2835 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2837 elsif File.exists?(destdir) && !File.directory?(destdir)
2838 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2840 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2841 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2843 system("mkdir '#{destdir}'")
2844 if !File.directory?(destdir)
2845 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2856 srcdir = from_utf8(src.text)
2857 destdir = from_utf8(dest.text)
2858 configskel = File.expand_path(from_utf8(conf.text))
2859 theme = theme_button.label
2860 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2861 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2862 opt432 = optimize432.active?
2863 madewith = madewithentry.text
2865 Thread.kill(src_nb_thread)
2866 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2869 Gtk.timeout_remove(timeout_src_nb)
2872 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2873 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2874 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' #{additional_booh_options}",
2875 utf8(_("Please wait while scanning source directory...")),
2877 { :closure_after => proc { open_file_user(configskel) } })
2882 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2884 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2885 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2886 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2888 source = $xmldoc.root.attributes['source']
2889 dest = $xmldoc.root.attributes['destination']
2890 theme = $xmldoc.root.attributes['theme']
2891 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2892 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2893 limit_sizes = $xmldoc.root.attributes['limit-sizes']
2895 limit_sizes = limit_sizes.split(/,/)
2897 madewith = $xmldoc.root.attributes['made-with']
2899 tooltips = Gtk::Tooltips.new
2900 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2901 tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2902 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2903 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2904 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2905 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2906 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2907 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
2908 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2909 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2910 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2911 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
2912 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2914 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2915 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2916 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2917 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2918 pack_start(sizes = Gtk::HBox.new, false, false, 0))
2919 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2920 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)
2921 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2922 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2923 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2924 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2926 madewithentry.text = madewith
2928 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)
2932 recreate_theme_config = proc {
2933 theme_sizes.each { |e| sizes.remove(e[:widget]) }
2935 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
2937 $images_size.each { |s|
2938 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2940 if limit_sizes.include?(s['name'])
2948 tooltips.set_tip(cb, utf8(s['description']), nil)
2949 theme_sizes << { :widget => cb, :value => s['name'] }
2951 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2952 tooltips = Gtk::Tooltips.new
2953 tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2954 if limit_sizes && limit_sizes.include?('original')
2957 theme_sizes << { :widget => cb, :value => 'original' }
2960 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2963 $allowed_N_values.each { |n|
2965 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2967 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2969 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2970 nperrowradios.add(Gtk::Label.new(' '))
2971 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
2974 nperrows << { :widget => rb, :value => n.to_s }
2976 nperrowradios.show_all
2978 recreate_theme_config.call
2980 theme_button.signal_connect('clicked') {
2981 if newtheme = theme_choose(theme_button.label)
2984 theme_button.label = newtheme
2985 recreate_theme_config.call
2989 dialog.vbox.add(frame1)
2990 dialog.vbox.add(frame2)
2991 dialog.window_position = Gtk::Window::POS_MOUSE
2997 dialog.run { |response|
2998 if response == Gtk::Dialog::RESPONSE_OK
2999 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3000 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3009 save_theme = theme_button.label
3010 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3011 save_opt432 = optimize432.active?
3012 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3013 save_madewith = madewithentry.text
3016 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith)
3017 mark_document_as_dirty
3019 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3020 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3021 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' #{additional_booh_options}",
3022 utf8(_("Please wait while scanning source directory...")),
3024 { :closure_after => proc {
3025 open_file($filename)
3034 sel = $albums_tv.selection.selected_rows
3036 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3037 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3038 utf8(_("Please wait while scanning source directory...")),
3040 { :closure_after => proc {
3041 open_file($filename)
3042 $albums_tv.selection.select_path(sel[0])
3050 sel = $albums_tv.selection.selected_rows
3052 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3053 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3054 utf8(_("Please wait while scanning source directory...")),
3056 { :closure_after => proc {
3057 open_file($filename)
3058 $albums_tv.selection.select_path(sel[0])
3066 theme = $xmldoc.root.attributes['theme']
3067 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3069 limit_sizes = "--sizes #{limit_sizes}"
3071 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3072 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3073 utf8(_("Please wait while scanning source directory...")),
3075 { :closure_after => proc {
3076 open_file($filename)
3082 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3084 Gtk::FileChooser::ACTION_SAVE,
3086 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3087 fc.transient_for = $main_window
3088 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3089 fc.set_current_folder(File.expand_path("~/.booh"))
3090 fc.filename = $orig_filename
3091 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3092 $orig_filename = fc.filename
3093 save_current_file_user
3094 $config['last-opens'] ||= []
3095 $config['last-opens'] << $orig_filename
3101 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3103 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3104 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3105 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3107 dialog.vbox.add(notebook = Gtk::Notebook.new)
3108 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3109 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3110 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3111 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
3112 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3113 tooltips = Gtk::Tooltips.new
3114 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3115 for example: /usr/bin/mplayer %f")), nil)
3116 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3117 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3118 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3119 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3120 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3121 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3122 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3123 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3124 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)),
3125 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3126 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)
3127 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3128 0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3129 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)
3130 tbl.attach(emptycomments_check = Gtk::CheckButton.new(utf8(_("Use empty comments for new albums"))),
3131 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3132 tooltips.set_tip(emptycomments_check, utf8(_("Normally, filenames are used as comments for new albums. Check this if you prefer empty comments.")), nil)
3133 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3134 0, 2, 5, 6, Gtk::FILL, Gtk::SHRINK, 2, 2)
3135 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)
3136 smp_check.signal_connect('toggled') {
3137 if smp_check.active?
3138 smp_hbox.sensitive = true
3140 smp_hbox.sensitive = false
3144 smp_check.active = true
3145 smp_spin.value = $config['mproc'].to_i
3147 nogestures_check.active = $config['nogestures']
3148 emptycomments_check.active = $config['emptycomments']
3149 deleteondisk_check.active = $config['deleteondisk']
3151 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3152 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3153 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3154 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3155 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3157 dialog.vbox.show_all
3158 dialog.run { |response|
3159 if response == Gtk::Dialog::RESPONSE_OK
3160 $config['video-viewer'] = video_viewer_entry.text
3161 $config['browser'] = browser_entry.text
3162 if smp_check.active?
3163 $config['mproc'] = smp_spin.value.to_i
3165 $config.delete('mproc')
3167 $config['nogestures'] = nogestures_check.active?
3168 $config['emptycomments'] = emptycomments_check.active?
3169 $config['deleteondisk'] = deleteondisk_check.active?
3171 $config['convert-enhance'] = enhance_entry.text
3178 if $undo_tb.sensitive?
3179 $redo_tb.sensitive = $redo_mb.sensitive = true
3180 if not more_undoes = UndoHandler.undo($statusbar)
3181 $undo_tb.sensitive = $undo_mb.sensitive = false
3187 if $redo_tb.sensitive?
3188 $undo_tb.sensitive = $undo_mb.sensitive = true
3189 if not more_redoes = UndoHandler.redo($statusbar)
3190 $redo_tb.sensitive = $redo_mb.sensitive = false
3195 def show_one_click_explanation(intro)
3196 show_popup($main_window, utf8(_("<b>One-Click tools.</b>
3198 %s When such a tool is activated
3199 (<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
3200 on a thumbnail will immediately apply the desired action.
3202 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
3208 GNU GENERAL PUBLIC LICENSE
3209 Version 2, June 1991
3211 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
3212 675 Mass Ave, Cambridge, MA 02139, USA
3213 Everyone is permitted to copy and distribute verbatim copies
3214 of this license document, but changing it is not allowed.
3218 The licenses for most software are designed to take away your
3219 freedom to share and change it. By contrast, the GNU General Public
3220 License is intended to guarantee your freedom to share and change free
3221 software--to make sure the software is free for all its users. This
3222 General Public License applies to most of the Free Software
3223 Foundation's software and to any other program whose authors commit to
3224 using it. (Some other Free Software Foundation software is covered by
3225 the GNU Library General Public License instead.) You can apply it to
3228 When we speak of free software, we are referring to freedom, not
3229 price. Our General Public Licenses are designed to make sure that you
3230 have the freedom to distribute copies of free software (and charge for
3231 this service if you wish), that you receive source code or can get it
3232 if you want it, that you can change the software or use pieces of it
3233 in new free programs; and that you know you can do these things.
3235 To protect your rights, we need to make restrictions that forbid
3236 anyone to deny you these rights or to ask you to surrender the rights.
3237 These restrictions translate to certain responsibilities for you if you
3238 distribute copies of the software, or if you modify it.
3240 For example, if you distribute copies of such a program, whether
3241 gratis or for a fee, you must give the recipients all the rights that
3242 you have. You must make sure that they, too, receive or can get the
3243 source code. And you must show them these terms so they know their
3246 We protect your rights with two steps: (1) copyright the software, and
3247 (2) offer you this license which gives you legal permission to copy,
3248 distribute and/or modify the software.
3250 Also, for each author's protection and ours, we want to make certain
3251 that everyone understands that there is no warranty for this free
3252 software. If the software is modified by someone else and passed on, we
3253 want its recipients to know that what they have is not the original, so
3254 that any problems introduced by others will not reflect on the original
3255 authors' reputations.
3257 Finally, any free program is threatened constantly by software
3258 patents. We wish to avoid the danger that redistributors of a free
3259 program will individually obtain patent licenses, in effect making the
3260 program proprietary. To prevent this, we have made it clear that any
3261 patent must be licensed for everyone's free use or not licensed at all.
3263 The precise terms and conditions for copying, distribution and
3264 modification follow.
3267 GNU GENERAL PUBLIC LICENSE
3268 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
3270 0. This License applies to any program or other work which contains
3271 a notice placed by the copyright holder saying it may be distributed
3272 under the terms of this General Public License. The "Program", below,
3273 refers to any such program or work, and a "work based on the Program"
3274 means either the Program or any derivative work under copyright law:
3275 that is to say, a work containing the Program or a portion of it,
3276 either verbatim or with modifications and/or translated into another
3277 language. (Hereinafter, translation is included without limitation in
3278 the term "modification".) Each licensee is addressed as "you".
3280 Activities other than copying, distribution and modification are not
3281 covered by this License; they are outside its scope. The act of
3282 running the Program is not restricted, and the output from the Program
3283 is covered only if its contents constitute a work based on the
3284 Program (independent of having been made by running the Program).
3285 Whether that is true depends on what the Program does.
3287 1. You may copy and distribute verbatim copies of the Program's
3288 source code as you receive it, in any medium, provided that you
3289 conspicuously and appropriately publish on each copy an appropriate
3290 copyright notice and disclaimer of warranty; keep intact all the
3291 notices that refer to this License and to the absence of any warranty;
3292 and give any other recipients of the Program a copy of this License
3293 along with the Program.
3295 You may charge a fee for the physical act of transferring a copy, and
3296 you may at your option offer warranty protection in exchange for a fee.
3298 2. You may modify your copy or copies of the Program or any portion
3299 of it, thus forming a work based on the Program, and copy and
3300 distribute such modifications or work under the terms of Section 1
3301 above, provided that you also meet all of these conditions:
3303 a) You must cause the modified files to carry prominent notices
3304 stating that you changed the files and the date of any change.
3306 b) You must cause any work that you distribute or publish, that in
3307 whole or in part contains or is derived from the Program or any
3308 part thereof, to be licensed as a whole at no charge to all third
3309 parties under the terms of this License.
3311 c) If the modified program normally reads commands interactively
3312 when run, you must cause it, when started running for such
3313 interactive use in the most ordinary way, to print or display an
3314 announcement including an appropriate copyright notice and a
3315 notice that there is no warranty (or else, saying that you provide
3316 a warranty) and that users may redistribute the program under
3317 these conditions, and telling the user how to view a copy of this
3318 License. (Exception: if the Program itself is interactive but
3319 does not normally print such an announcement, your work based on
3320 the Program is not required to print an announcement.)
3323 These requirements apply to the modified work as a whole. If
3324 identifiable sections of that work are not derived from the Program,
3325 and can be reasonably considered independent and separate works in
3326 themselves, then this License, and its terms, do not apply to those
3327 sections when you distribute them as separate works. But when you
3328 distribute the same sections as part of a whole which is a work based
3329 on the Program, the distribution of the whole must be on the terms of
3330 this License, whose permissions for other licensees extend to the
3331 entire whole, and thus to each and every part regardless of who wrote it.
3333 Thus, it is not the intent of this section to claim rights or contest
3334 your rights to work written entirely by you; rather, the intent is to
3335 exercise the right to control the distribution of derivative or
3336 collective works based on the Program.
3338 In addition, mere aggregation of another work not based on the Program
3339 with the Program (or with a work based on the Program) on a volume of
3340 a storage or distribution medium does not bring the other work under
3341 the scope of this License.
3343 3. You may copy and distribute the Program (or a work based on it,
3344 under Section 2) in object code or executable form under the terms of
3345 Sections 1 and 2 above provided that you also do one of the following:
3347 a) Accompany it with the complete corresponding machine-readable
3348 source code, which must be distributed under the terms of Sections
3349 1 and 2 above on a medium customarily used for software interchange; or,
3351 b) Accompany it with a written offer, valid for at least three
3352 years, to give any third party, for a charge no more than your
3353 cost of physically performing source distribution, a complete
3354 machine-readable copy of the corresponding source code, to be
3355 distributed under the terms of Sections 1 and 2 above on a medium
3356 customarily used for software interchange; or,
3358 c) Accompany it with the information you received as to the offer
3359 to distribute corresponding source code. (This alternative is
3360 allowed only for noncommercial distribution and only if you
3361 received the program in object code or executable form with such
3362 an offer, in accord with Subsection b above.)
3364 The source code for a work means the preferred form of the work for
3365 making modifications to it. For an executable work, complete source
3366 code means all the source code for all modules it contains, plus any
3367 associated interface definition files, plus the scripts used to
3368 control compilation and installation of the executable. However, as a
3369 special exception, the source code distributed need not include
3370 anything that is normally distributed (in either source or binary
3371 form) with the major components (compiler, kernel, and so on) of the
3372 operating system on which the executable runs, unless that component
3373 itself accompanies the executable.
3375 If distribution of executable or object code is made by offering
3376 access to copy from a designated place, then offering equivalent
3377 access to copy the source code from the same place counts as
3378 distribution of the source code, even though third parties are not
3379 compelled to copy the source along with the object code.
3382 4. You may not copy, modify, sublicense, or distribute the Program
3383 except as expressly provided under this License. Any attempt
3384 otherwise to copy, modify, sublicense or distribute the Program is
3385 void, and will automatically terminate your rights under this License.
3386 However, parties who have received copies, or rights, from you under
3387 this License will not have their licenses terminated so long as such
3388 parties remain in full compliance.
3390 5. You are not required to accept this License, since you have not
3391 signed it. However, nothing else grants you permission to modify or
3392 distribute the Program or its derivative works. These actions are
3393 prohibited by law if you do not accept this License. Therefore, by
3394 modifying or distributing the Program (or any work based on the
3395 Program), you indicate your acceptance of this License to do so, and
3396 all its terms and conditions for copying, distributing or modifying
3397 the Program or works based on it.
3399 6. Each time you redistribute the Program (or any work based on the
3400 Program), the recipient automatically receives a license from the
3401 original licensor to copy, distribute or modify the Program subject to
3402 these terms and conditions. You may not impose any further
3403 restrictions on the recipients' exercise of the rights granted herein.
3404 You are not responsible for enforcing compliance by third parties to
3407 7. If, as a consequence of a court judgment or allegation of patent
3408 infringement or for any other reason (not limited to patent issues),
3409 conditions are imposed on you (whether by court order, agreement or
3410 otherwise) that contradict the conditions of this License, they do not
3411 excuse you from the conditions of this License. If you cannot
3412 distribute so as to satisfy simultaneously your obligations under this
3413 License and any other pertinent obligations, then as a consequence you
3414 may not distribute the Program at all. For example, if a patent
3415 license would not permit royalty-free redistribution of the Program by
3416 all those who receive copies directly or indirectly through you, then
3417 the only way you could satisfy both it and this License would be to
3418 refrain entirely from distribution of the Program.
3420 If any portion of this section is held invalid or unenforceable under
3421 any particular circumstance, the balance of the section is intended to
3422 apply and the section as a whole is intended to apply in other
3425 It is not the purpose of this section to induce you to infringe any
3426 patents or other property right claims or to contest validity of any
3427 such claims; this section has the sole purpose of protecting the
3428 integrity of the free software distribution system, which is
3429 implemented by public license practices. Many people have made
3430 generous contributions to the wide range of software distributed
3431 through that system in reliance on consistent application of that
3432 system; it is up to the author/donor to decide if he or she is willing
3433 to distribute software through any other system and a licensee cannot
3436 This section is intended to make thoroughly clear what is believed to
3437 be a consequence of the rest of this License.
3440 8. If the distribution and/or use of the Program is restricted in
3441 certain countries either by patents or by copyrighted interfaces, the
3442 original copyright holder who places the Program under this License
3443 may add an explicit geographical distribution limitation excluding
3444 those countries, so that distribution is permitted only in or among
3445 countries not thus excluded. In such case, this License incorporates
3446 the limitation as if written in the body of this License.
3448 9. The Free Software Foundation may publish revised and/or new versions
3449 of the General Public License from time to time. Such new versions will
3450 be similar in spirit to the present version, but may differ in detail to
3451 address new problems or concerns.
3453 Each version is given a distinguishing version number. If the Program
3454 specifies a version number of this License which applies to it and "any
3455 later version", you have the option of following the terms and conditions
3456 either of that version or of any later version published by the Free
3457 Software Foundation. If the Program does not specify a version number of
3458 this License, you may choose any version ever published by the Free Software
3461 10. If you wish to incorporate parts of the Program into other free
3462 programs whose distribution conditions are different, write to the author
3463 to ask for permission. For software which is copyrighted by the Free
3464 Software Foundation, write to the Free Software Foundation; we sometimes
3465 make exceptions for this. Our decision will be guided by the two goals
3466 of preserving the free status of all derivatives of our free software and
3467 of promoting the sharing and reuse of software generally.
3471 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
3472 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
3473 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
3474 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
3475 OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
3476 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
3477 TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
3478 PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
3479 REPAIR OR CORRECTION.
3481 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
3482 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
3483 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
3484 INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
3485 OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
3486 TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
3487 YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
3488 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
3489 POSSIBILITY OF SUCH DAMAGES.
3493 def create_menu_and_toolbar
3496 mb = Gtk::MenuBar.new
3498 filemenu = Gtk::MenuItem.new(utf8(_("_File")))
3499 filesubmenu = Gtk::Menu.new
3500 filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
3501 filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
3502 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3503 filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
3504 filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
3505 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3506 tooltips = Gtk::Tooltips.new
3507 filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
3508 $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3509 tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
3510 filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
3511 $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3512 tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
3513 filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
3514 $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3515 tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums (anywhere)")), nil)
3516 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3517 filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
3518 $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3519 tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
3520 filesubmenu.append($view_wa = Gtk::ImageMenuItem.new(utf8(_("View web-album with browser"))).set_sensitive(false))
3521 $view_wa.image = Gtk::Image.new("#{$FPATH}/images/stock-view-webalbum-16.png")
3522 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3523 filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
3524 tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
3525 filesubmenu.append( Gtk::SeparatorMenuItem.new)
3526 filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
3527 filemenu.set_submenu(filesubmenu)
3530 new.signal_connect('activate') { new_album }
3531 open.signal_connect('activate') { open_file_popup }
3532 $save.signal_connect('activate') { save_current_file_user }
3533 $save_as.signal_connect('activate') { save_as_do }
3534 $merge_current.signal_connect('activate') { merge_current }
3535 $merge_newsubs.signal_connect('activate') { merge_newsubs }
3536 $merge.signal_connect('activate') { merge }
3537 $generate.signal_connect('activate') {
3539 call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
3540 utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
3542 { :successmsg => utf8(_("Your web-album is now ready in directory '%s'.
3543 Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
3544 :successmsg_linkurl => $xmldoc.root.attributes['destination'],
3545 :closure_after => proc {
3546 $xmldoc.elements.each('//dir') { |elem|
3547 $modified ||= elem.attributes['already-generated'].nil?
3548 elem.add_attribute('already-generated', 'true')
3550 UndoHandler.cleanup #- prevent save_changes to mark current dir as not already generated
3551 $undo_tb.sensitive = $undo_mb.sensitive = false
3552 $redo_tb.sensitive = $redo_mb.sensitive = false
3554 $generated_outofline = true
3557 $view_wa.signal_connect('activate') {
3558 indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
3559 if File.exists?(indexhtml)
3562 show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
3565 $properties.signal_connect('activate') { properties }
3567 quit.signal_connect('activate') { try_quit }
3569 editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
3570 editsubmenu = Gtk::Menu.new
3571 editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
3572 editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
3573 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3574 editsubmenu.append($sort_by_exif_date = Gtk::ImageMenuItem.new(utf8(_("Sort by EXIF date"))).set_sensitive(false))
3575 $sort_by_exif_date.image = Gtk::Image.new("#{$FPATH}/images/sort_by_exif_date.png")
3576 editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
3577 $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
3578 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)
3579 editsubmenu.append( Gtk::SeparatorMenuItem.new)
3580 editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
3581 editmenu.set_submenu(editsubmenu)
3584 $remove_all_captions.signal_connect('activate') { remove_all_captions }
3585 $sort_by_exif_date.signal_connect('activate') { sort_by_exif_date }
3587 prefs.signal_connect('activate') { preferences }
3589 helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
3590 helpsubmenu = Gtk::Menu.new
3591 helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
3592 one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
3593 helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
3594 speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
3595 helpsubmenu.append(tutos = Gtk::ImageMenuItem.new(utf8(_("Online tutorials (opens a web-browser)"))))
3596 tutos.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3597 helpsubmenu.append( Gtk::SeparatorMenuItem.new)
3598 helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
3599 helpmenu.set_submenu(helpsubmenu)
3602 one_click.signal_connect('activate') {
3603 show_one_click_explanation(_("One-Click tools are available in the toolbar."))
3606 speed.signal_connect('activate') {
3607 show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
3609 <span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
3610 <span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
3611 <span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
3612 <span foreground='darkblue'>Control-Enter</span>: for an image, open larger view; for a video, launch player
3613 <span foreground='darkblue'>Control-Delete</span>: delete image
3614 <span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
3615 <span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
3616 <span foreground='darkblue'>Control-z</span>: undo
3617 <span foreground='darkblue'>Control-r</span>: redo
3619 <span size='large' weight='bold'>Mouse gestures:</span>
3621 Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
3622 for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
3624 <span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
3625 <span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
3626 <span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
3627 <span foreground='darkblue'>Left click, hold left button, right click</span>: undo
3628 <span foreground='darkblue'>Right click, hold right button, left click</span>: redo
3629 ")), { :pos_centered => true, :not_transient => true })
3632 tutos.signal_connect('activate') {
3633 open_url('http://zarb.org/~gc/html/booh/tutorial.html')
3636 about.signal_connect('activate') {
3637 Gtk::AboutDialog.set_url_hook { |dialog, url| open_url(url) }
3638 Gtk::AboutDialog.show($main_window, { :name => 'booh',
3639 :version => $VERSION,
3640 :copyright => 'Copyright (c) 2005 Guillaume Cottenceau',
3641 :license => get_license,
3642 :website => 'http://zarb.org/~gc/html/booh.html',
3643 :authors => [ 'Guillaume Cottenceau' ],
3644 :artists => [ 'Ayo73' ],
3645 :comments => utf8(_("''The Web-Album of choice for discriminating Linux users''")),
3646 :translator_credits => utf8(_('Japanese: Masao Mutoh
3647 German: Roland Eckert
3648 French: Guillaume Cottenceau')),
3649 :logo => Gdk::Pixbuf.new("#{$FPATH}/images/logo.png") })
3654 tb = Gtk::Toolbar.new
3656 tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
3657 open.label = utf8(_("Open")) #- to avoid missing gtk2 l10n catalogs
3658 open.menu = Gtk::Menu.new
3659 open.signal_connect('clicked') { open_file_popup }
3660 open.signal_connect('show-menu') {
3661 lastopens = Gtk::Menu.new
3663 if $config['last-opens']
3664 $config['last-opens'].reverse.each { |e|
3665 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
3666 item.signal_connect('activate') {
3667 if ask_save_modifications(utf8(_("Save this album?")),
3668 utf8(_("Do you want to save the changes to this album?")),
3669 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3670 push_mousecursor_wait
3671 msg = open_file_user(from_utf8(e))
3674 show_popup($main_window, msg)
3682 open.menu = lastopens
3685 tb.insert(-1, Gtk::SeparatorToolItem.new)
3687 tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
3688 $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
3689 $r90.label = utf8(_("Rotate"))
3690 tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
3691 $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
3692 $r270.label = utf8(_("Rotate"))
3693 tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
3694 $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
3695 $enhance.label = utf8(_("Enhance"))
3696 tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
3697 $delete.label = utf8(_("Delete")) #- to avoid missing gtk2 l10n catalogs
3698 tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
3699 nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
3700 nothing.label = utf8(_("None"))
3702 tb.insert(-1, Gtk::SeparatorToolItem.new)
3704 tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
3705 tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
3708 $undo_tb.signal_connect('clicked') { perform_undo }
3709 $undo_mb.signal_connect('activate') { perform_undo }
3710 $redo_tb.signal_connect('clicked') { perform_redo }
3711 $redo_mb.signal_connect('activate') { perform_redo }
3713 one_click_explain_try = Proc.new {
3714 if !$config['one-click-explained']
3715 show_one_click_explanation(_("You have just clicked on a One-Click tool."))
3716 $config['one-click-explained'] = true
3720 $r90.signal_connect('toggled') {
3722 set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
3723 one_click_explain_try.call
3724 $r270.active = false
3725 $enhance.active = false
3726 $delete.active = false
3727 nothing.sensitive = true
3729 if !$r270.active? && !$enhance.active? && !$delete.active?
3730 set_mousecursor_normal
3731 nothing.sensitive = false
3733 nothing.sensitive = true
3737 $r270.signal_connect('toggled') {
3739 set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
3740 one_click_explain_try.call
3742 $enhance.active = false
3743 $delete.active = false
3744 nothing.sensitive = true
3746 if !$r90.active? && !$enhance.active? && !$delete.active?
3747 set_mousecursor_normal
3748 nothing.sensitive = false
3750 nothing.sensitive = true
3754 $enhance.signal_connect('toggled') {
3756 set_mousecursor(Gdk::Cursor::SPRAYCAN)
3757 one_click_explain_try.call
3759 $r270.active = false
3760 $delete.active = false
3761 nothing.sensitive = true
3763 if !$r90.active? && !$r270.active? && !$delete.active?
3764 set_mousecursor_normal
3765 nothing.sensitive = false
3767 nothing.sensitive = true
3771 $delete.signal_connect('toggled') {
3773 set_mousecursor(Gdk::Cursor::PIRATE)
3774 one_click_explain_try.call
3776 $r270.active = false
3777 $enhance.active = false
3778 nothing.sensitive = true
3780 if !$r90.active? && !$r270.active? && !$enhance.active?
3781 set_mousecursor_normal
3782 nothing.sensitive = false
3784 nothing.sensitive = true
3788 nothing.signal_connect('clicked') {
3789 $r90.active = $r270.active = $enhance.active = $delete.active = false
3790 set_mousecursor_normal
3796 def gtk_thread_protect(&proc)
3797 if Thread.current == Thread.main
3800 $protect_gtk_pending_calls.synchronize {
3801 $gtk_pending_calls << proc
3806 def gtk_thread_flush
3807 $protect_gtk_pending_calls.try_lock
3808 for closure in $gtk_pending_calls
3811 $gtk_pending_calls = []
3812 $protect_gtk_pending_calls.unlock
3815 def ask_password_protect
3816 value = $xmldir.attributes['password-protect']
3818 dialog = Gtk::Dialog.new(utf8(_("Password protect this sub-album")),
3820 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3821 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3822 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3824 lbl = Gtk::Label.new
3826 _("You can choose to <b>password protect</b> the sub-album '%s' (only available
3827 if you plan to publish your web-album with an Apache web-server). This will use
3828 the .htaccess/.htpasswd feature of Apache (not so strongly crypted password, but
3829 generally ok for protecting web contents). Users will be prompted with a dialog
3830 asking for a username and a password, failure to give the correct pair will
3832 ") % File.basename($current_path))
3833 dialog.vbox.add(lbl)
3834 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("free access")))).
3835 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("password protect with password file:")))).
3836 add(file = Gtk::Entry.new)))
3837 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0.5, 0.2).add(Gtk::HBox.new.add(bt_help = Gtk::Button.new(utf8(_("help about password file")))).
3838 add(Gtk::Label.new).
3839 add(bt_gen = Gtk::Button.new(utf8(_("generate a password file"))))))
3840 dialog.window_position = Gtk::Window::POS_MOUSE
3845 rb_yes.active = true
3849 bt_help.signal_connect('clicked') {
3850 show_popup(dialog, utf8(
3851 _("Password protection proposed here uses the .htaccess/.htpasswd features
3852 proposed by Apache. So first, be sure you will publish your web-album on an
3853 Apache web-server. Second, you will need to have a .htpasswd file accessible
3854 by Apache somewhere on the web-server disks. The password file you must
3855 provide in the dialog when choosing to password protect is the full absolute
3856 path to access this file <b>on the web-server</b> (not on your machine). Note
3857 that if you use a relative path, it will be considered relative to the
3858 Document Root of the Apache configuration.")))
3861 bt_gen.signal_connect('clicked') {
3862 gendialog = Gtk::Dialog.new(utf8(_("Generate a password file")),
3864 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3865 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3866 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3868 lbl = Gtk::Label.new
3870 _("I can generate a password file (.htpasswd for Apache) for you. Just type
3871 the username and password you wish to put in it below and validate."))
3872 gendialog.vbox.add(lbl)
3873 gendialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(Gtk::Label.new(utf8(_('Username:')))).
3874 add(user = Gtk::Entry.new).
3875 add(Gtk::Label.new(utf8(_('Password:')))).
3876 add(pass = Gtk::Entry.new)))
3877 pass.visibility = false
3878 gendialog.window_position = Gtk::Window::POS_MOUSE
3880 gendialog.run { |response|
3884 if response == Gtk::Dialog::RESPONSE_OK
3886 ary = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a + [ '.', '/' ]
3887 return ary[rand(ary.length)]
3889 fout = Tempfile.new("htpasswd")
3890 fout.write("#{u}:#{p.crypt(rand_letter + rand_letter)}\n")
3892 File.chmod(0644, fout.path)
3893 show_popup(dialog, utf8(
3894 _("The file <b>%s</b> now contains the username and the crypted password. Now
3895 copy it to a suitable location on the machine hosting the Apache web-server (better not
3896 below the Document Root), and specify this location in the password protect dialog.") % fout.path), { :selectable => true })
3901 dialog.run { |response|
3908 if response == Gtk::Dialog::RESPONSE_OK && value != newval
3910 msg 3, "changing password protection of #{$current_path} to #{newval}"
3912 $xmldir.delete_attribute('password-protect')
3914 $xmldir.add_attribute('password-protect', newval)
3916 save_undo(_("set password protection for %s") % File.basename($current_path),
3919 $xmldir.delete_attribute('password-protect')
3921 $xmldir.add_attribute('password-protect', value)
3925 $xmldir.delete_attribute('password-protect')
3927 $xmldir.add_attribute('password-protect', newval)
3931 show_password_protections
3936 def create_main_window
3938 mb, tb = create_menu_and_toolbar
3940 $albums_tv = Gtk::TreeView.new
3941 $albums_tv.set_size_request(120, -1)
3942 $albums_tv.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }))
3943 $albums_tv.append_column(tcol = Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, { :text => 0 }))
3944 $albums_tv.expander_column = tcol
3945 $albums_tv.set_headers_visible(false)
3946 $albums_tv.selection.signal_connect('changed') { |w|
3947 push_mousecursor_wait
3951 msg 3, "no selection"
3953 $current_path = $albums_ts.get_value(iter, 1)
3958 $albums_tv.signal_connect('button-release-event') { |w, event|
3959 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3 && !$current_path.nil?
3960 menu = Gtk::Menu.new
3961 menu.append(passprotect = Gtk::ImageMenuItem.new(utf8(_("Password protect"))))
3962 passprotect.image = Gtk::Image.new("#{$FPATH}/images/galeon-secure.png")
3963 passprotect.signal_connect('activate') { ask_password_protect }
3964 menu.append(restore = Gtk::ImageMenuItem.new(utf8(_("Restore deleted images/videos/subalbums"))))
3965 restore.image = Gtk::Image.new("#{$FPATH}/images/restore.png")
3966 restore.signal_connect('activate') { restore_deleted }
3967 menu.append(Gtk::SeparatorMenuItem.new)
3968 menu.append(delete = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
3969 delete.signal_connect('activate') {
3970 if show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed? This operation cannot be undone.")), { :okcancel => true })
3971 delete_current_subalbum
3975 menu.popup(nil, nil, event.button, event.time)
3979 $albums_ts = Gtk::TreeStore.new(String, String, Gdk::Pixbuf)
3980 $albums_tv.set_model($albums_ts)
3981 $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
3983 albums_sw = Gtk::ScrolledWindow.new(nil, nil)
3984 albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
3985 albums_sw.add_with_viewport($albums_tv)
3987 $notebook = Gtk::Notebook.new
3988 create_subalbums_page
3989 $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
3991 $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
3993 $notebook.signal_connect('switch-page') { |w, page, num|
3995 $delete.active = false
3996 $delete.sensitive = false
3998 $delete.sensitive = true
4000 if $xmldir && $subalbums_edits[$xmldir.attributes['path']] && textview = $subalbums_edits[$xmldir.attributes['path']][:editzone]
4002 textview.buffer.text = $thumbnails_title.buffer.text
4004 if $notebook.get_tab_label($autotable_sw).sensitive?
4005 $thumbnails_title.buffer.text = textview.buffer.text
4011 paned = Gtk::HPaned.new
4012 paned.pack1(albums_sw, false, false)
4013 paned.pack2($notebook, true, true)
4015 main_vbox = Gtk::VBox.new(false, 0)
4016 main_vbox.pack_start(mb, false, false)
4017 main_vbox.pack_start(tb, false, false)
4018 main_vbox.pack_start(paned, true, true)
4019 main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
4021 $main_window = Gtk::Window.new
4022 $main_window.add(main_vbox)
4023 $main_window.signal_connect('delete-event') {
4024 try_quit({ :disallow_cancel => true })
4027 #- read/save size and position of window
4028 if $config['pos-x'] && $config['pos-y']
4029 $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
4031 $main_window.window_position = Gtk::Window::POS_CENTER
4033 msg 3, "size: #{$config['width']}x#{$config['height']}"
4034 $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
4035 $main_window.signal_connect('configure-event') {
4036 msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
4037 x, y = $main_window.window.root_origin
4038 width, height = $main_window.window.size
4039 $config['pos-x'] = x
4040 $config['pos-y'] = y
4041 $config['width'] = width
4042 $config['height'] = height
4046 $protect_gtk_pending_calls = Mutex.new
4047 $gtk_pending_calls = []
4048 Gtk.timeout_add(100) {
4051 $protect_gtk_pending_calls.synchronize {
4052 if ! $gtk_pending_calls.empty?
4053 $gtk_pending_calls.shift.call
4055 empty = $gtk_pending_calls.empty?
4061 $statusbar.push(0, utf8(_("Ready.")))
4062 $main_window.show_all
4065 Thread.abort_on_exception = true
4075 open_file_user(ARGV[0])