1d9666fc5163b149dd9a0319e22e6168dae439ee
[booh] / bin / booh
1 #! /usr/bin/ruby
2 #
3 #                         *  BOOH  *
4 #
5 # A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
6 #
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.
11 #
12 #
13 # Copyright (c) 2004-2008 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
14 #
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
17 #
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21
22 require 'getoptlong'
23 require 'tempfile'
24 require 'thread'
25
26 require 'gtk2'
27 require 'booh/libadds'
28 require 'booh/GtkAutoTable'
29
30 require 'gettext'
31 include GetText
32 bindtextdomain("booh")
33
34 require 'booh/rexml/document'
35 include REXML
36
37 require 'booh/booh-lib'
38 include Booh
39 require 'booh/UndoHandler'
40 require 'booh/Synchronizator'
41
42
43 #- options
44 $options = [
45     [ '--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)") ],
47     [ '--version',       '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
48 ]
49
50 #- default values for some globals 
51 $xmldir = nil
52 $xmlaccesslock = Object.new
53 $modified = false
54 $current_cursor = nil
55 $ignore_videos = false
56 $button1_pressed_autotable = false
57 $generated_outofline = false
58
59 def usage
60     puts _("Usage: %s [OPTION]...") % File.basename($0)
61     $options.each { |ary|
62         printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
63     }
64 end
65
66 def handle_options
67     parser = GetoptLong.new
68     parser.set_options(*$options.collect { |ary| ary[0..2] })
69     begin
70         parser.each_option do |name, arg|
71             case name
72             when '--help'
73                 usage
74                 exit(0)
75
76             when '--version'
77                 puts _("Booh version %s
78
79 Copyright (c) 2005-2008 Guillaume Cottenceau.
80 This is free software; see the source for copying conditions.  There is NO
81 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
82
83                 exit(0)
84
85             when '--verbose-level'
86                 $verbose_level = arg.to_i
87
88             end
89         end
90     rescue
91         puts $!
92         usage
93         exit(1)
94     end
95 end
96
97 def read_config
98     $config = {}
99     $config_file = File.expand_path('~/.booh-gui-rc')
100     if File.readable?($config_file)
101         $xmldoc = Synchronizator.new(REXML::Document.new(File.new($config_file)), $xmlaccesslock)
102         $xmldoc.root.elements.each { |element|
103             txt = element.get_text
104             if txt
105                 if txt.value =~ /~~~/ || element.name == 'last-opens'
106                     $config[element.name] = txt.value.split(/~~~/)
107                 else
108                     $config[element.name] = txt.value
109                 end
110             elsif element.elements.size == 0
111                 $config[element.name] = ''
112             else
113                 $config[element.name] = {}
114                 element.each { |chld|
115                     txt = chld.get_text
116                     $config[element.name][chld.name] = txt ? txt.value : nil
117                 }
118             end
119         }
120     end
121     $config['video-viewer'] ||= '/usr/bin/mplayer %f'
122     $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
123     $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
124     $config['comments-format'] ||= '%t'
125     if !FileTest.directory?(File.expand_path('~/.booh'))
126         system("mkdir ~/.booh")
127     end
128     if $config['mproc'].nil?
129         cpus = 0
130         for line in IO.readlines('/proc/cpuinfo') do
131             line =~ /^processor/ and cpus += 1
132         end
133         if cpus > 1
134             $config['mproc'] = cpus
135         end
136     end
137     $config['rotate-set-exif'] ||= 'true'
138     $tempfiles = []
139     $todelete = []
140 end
141
142 def check_config
143     if !system("which convert >/dev/null 2>/dev/null")
144         show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
145 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
146         exit 1
147     end
148     if !system("which identify >/dev/null 2>/dev/null")
149         show_popup($main_window, utf8(_("The program 'identify' is needed to get photos sizes and EXIF data. Please install it.
150 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
151     end
152     if !system("which exif >/dev/null 2>/dev/null")
153         show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
154     end
155     missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
156     if missing != []
157         show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
158     end
159
160     viewer_binary = $config['video-viewer'].split.first
161     if viewer_binary && !File.executable?(viewer_binary)
162         show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
163 You should fix this in Edit/Preferences so that you can view videos.
164
165 Problem was: '%s' is not an executable file.
166 Hint: don't forget to specify the full path to the executable,
167 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
168     end
169 end
170
171 def check_image_editor
172     image_editor_binary = $config['image-editor'].split.first
173     if image_editor_binary && !File.executable?(image_editor_binary)
174         show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
175 You should fix this in Edit/Preferences so that you can edit photos externally.
176
177 Problem was: '%s' is not an executable file.
178 Hint: don't forget to specify the full path to the executable,
179 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
180         return false
181     else
182         return true
183     end
184 end
185
186 def write_config
187     if $config['last-opens'] && $config['last-opens'].size > 10
188         $config['last-opens'] = $config['last-opens'][-10, 10]
189     end
190
191     $xmldoc = Synchronizator.new(Document.new("<booh-gui-rc version='#{$VERSION}'/>"), $xmlaccesslock)
192     $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
193     $config.each_pair { |key, value|
194         elem = $xmldoc.root.add_element key
195         if value.is_a? Hash
196             $config[key].each_pair { |subkey, subvalue|
197                 subelem = elem.add_element subkey
198                 subelem.add_text subvalue.to_s
199             }
200         elsif value.is_a? Array
201             elem.add_text value.join('~~~')
202         else
203             if !value
204                 elem.remove
205             else
206                 elem.add_text value.to_s
207             end
208         end
209     }
210     ios = File.open($config_file, "w")
211     $xmldoc.write(ios, 0)
212     ios.close
213
214     $tempfiles.each { |f|
215         if File.exists?(f)
216             File.delete(f)
217         end
218     }
219 end
220
221 def set_mousecursor(what, *widget)
222     cursor = what.nil? ? nil : Gdk::Cursor.new(what)
223     if widget[0] && widget[0].window
224         widget[0].window.cursor = cursor
225     end
226     if $main_window && $main_window.window
227         $main_window.window.cursor = cursor
228     end
229     $current_cursor = what
230 end
231 def set_mousecursor_wait(*widget)
232     gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
233     if Thread.current == Thread.main
234         Gtk.main_iteration while Gtk.events_pending?
235     end
236 end
237 def set_mousecursor_normal(*widget)
238     gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
239 end
240 def push_mousecursor_wait(*widget)
241     if $current_cursor != Gdk::Cursor::WATCH
242         $save_cursor = $current_cursor
243         gtk_thread_protect { set_mousecursor_wait(*widget) }
244     end
245 end
246 def pop_mousecursor(*widget)
247     gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
248 end
249
250 def current_dest_dir
251     source = $xmldoc.root.attributes['source']
252     dest = $xmldoc.root.attributes['destination']
253     return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
254 end
255
256 def full_src_dir_to_rel(path, source)
257     return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
258 end
259
260 def build_full_dest_filename(filename)
261     return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
262 end
263
264 def save_undo(name, closure, *params)
265     UndoHandler.save_undo(name, closure, [ *params ])
266     $undo_tb.sensitive = $undo_mb.sensitive = true
267     $redo_tb.sensitive = $redo_mb.sensitive = false
268 end
269
270 def view_element(filename, closures)
271     if entry2type(filename) == 'video'
272         cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
273         msg 2, cmd
274         system(cmd)
275         return
276     end
277
278     w = create_window.set_title(filename)
279
280     msg 3, "filename: #{filename}"
281     dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
282     #- typically this file won't exist in case of videos; try with the largest thumbnail around
283     if !File.exists?(dest_img)
284         if entry2type(filename) == 'video'
285             alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
286             if not alternatives.empty?
287                 dest_img = alternatives[-1]
288             end
289         else
290             push_mousecursor_wait
291             gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
292             pop_mousecursor
293             if !File.exists?(dest_img)
294                 msg 2, _("Could not generate fullscreen thumbnail!")
295                 return
296                 end
297         end
298     end
299     aspect = utf8(_("Aspect: unknown"))
300     size = get_image_size(from_utf8("#{$current_path}/#{filename}"))
301     if size
302         aspect = utf8(_("Aspect: %s") % sprintf("%1.3f", size[:x].to_f/size[:y]))
303     end
304     vbox = Gtk::VBox.new.add(Gtk::Image.new(dest_img)).add(Gtk::Label.new.set_markup("<i>#{aspect}</i>"))
305     evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(vbox).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
306     evt.signal_connect('button-press-event') { |this, event|
307         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
308             $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
309         end
310         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
311             menu = Gtk::Menu.new
312             menu.append(delete_item  = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
313             delete_item.signal_connect('activate') {
314                 w.destroy
315                 closures[:delete].call(false)
316             }
317             menu.show_all
318             menu.popup(nil, nil, event.button, event.time)
319         end
320     }
321     evt.signal_connect('button-release-event') { |this, event|
322         if $gesture_press
323             if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
324                 msg 3, "gesture delete: click-drag right button to the bottom"
325                 w.destroy
326                 closures[:delete].call(false)
327                 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
328             end
329         end
330     }
331     tooltips = Gtk::Tooltips.new
332     tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
333
334     w.signal_connect('key-press-event') { |w,event|
335         if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
336             w.destroy
337             closures[:delete].call(false)
338         end
339     }
340
341     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
342     b.signal_connect('clicked') { w.destroy }
343
344     vb = Gtk::VBox.new
345     vb.pack_start(evt, false, false)
346     vb.pack_end(bottom, false, false)
347
348     w.add(vb)
349     w.signal_connect('delete-event') { w.destroy }
350     w.window_position = Gtk::Window::POS_CENTER
351     w.show_all
352 end
353
354 def scroll_upper(scrolledwindow, ypos_top)
355     newval = scrolledwindow.vadjustment.value -
356         ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
357     if newval < scrolledwindow.vadjustment.lower
358         newval = scrolledwindow.vadjustment.lower
359     end
360     scrolledwindow.vadjustment.value = newval
361 end
362
363 def scroll_lower(scrolledwindow, ypos_bottom)
364     newval = scrolledwindow.vadjustment.value +
365         ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
366     if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
367         newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
368     end
369     scrolledwindow.vadjustment.value = newval
370 end
371
372 def autoscroll_if_needed(scrolledwindow, image, textview)
373     #- autoscroll if cursor or image is not visible, if possible
374     if image && image.window || textview.window
375         ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
376         ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
377         current_miny_visible = scrolledwindow.vadjustment.value
378         current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
379         if ypos_top < current_miny_visible
380             scroll_upper(scrolledwindow, ypos_top)
381         elsif ypos_bottom > current_maxy_visible
382             scroll_lower(scrolledwindow, ypos_bottom)
383         end
384     end
385 end
386
387 def create_editzone(scrolledwindow, pagenum, image)
388     frame = Gtk::Frame.new
389     frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
390     frame.set_shadow_type(Gtk::SHADOW_IN)
391     textview.signal_connect('key-press-event') { |w, event|
392         textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
393         if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
394             scrolledwindow.signal_emit('key-press-event', event)
395         end
396         if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
397            event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
398             if event.keyval == Gdk::Keyval::GDK_Up
399                 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
400                     scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
401                 else
402                     scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
403                 end
404             else
405                 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
406                     scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
407                 else
408                     scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
409                 end
410             end
411         end
412         false  #- propagate
413     }
414
415     candidate_undo_text = nil
416     textview.signal_connect('focus-in-event') { |w, event|
417         textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
418         candidate_undo_text = textview.buffer.text
419         false  #- propagate
420     }
421
422     textview.signal_connect('key-release-event') { |w, event|
423         if candidate_undo_text && candidate_undo_text != textview.buffer.text
424             $modified = true
425             save_undo(_("text edit"),
426                       proc { |text|
427                           save_text = textview.buffer.text
428                           textview.buffer.text = text
429                           textview.grab_focus
430                           $notebook.set_page(pagenum)
431                           proc {
432                               textview.buffer.text = save_text
433                               textview.grab_focus
434                               $notebook.set_page(pagenum)
435                           }
436                       }, candidate_undo_text)
437             candidate_undo_text = nil
438         end
439
440         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)
441             autoscroll_if_needed(scrolledwindow, image, textview)
442         end
443         false  #- propagate
444     }
445
446     return [ frame, textview ]
447 end
448
449 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
450
451     if !$modified_pixbufs[thumbnail_img]
452         $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
453     elsif !$modified_pixbufs[thumbnail_img][:orig]
454         $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
455     end
456
457     pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
458
459     #- rotate
460     if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
461         pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
462         msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
463         if pixbuf.height > desired_y
464             pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
465         elsif pixbuf.width < desired_x && pixbuf.height < desired_y
466             pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
467         end
468     end
469
470     #- fix white balance
471     if $modified_pixbufs[thumbnail_img][:whitebalance]
472         pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
473     end
474
475     #- fix gamma correction
476     if $modified_pixbufs[thumbnail_img][:gammacorrect]
477         pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
478     end
479
480     img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
481 end
482
483 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
484     $modified = true
485
486     #- update rotate attribute
487     new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
488     xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
489
490     if $config['rotate-set-exif'] == 'true'
491         Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
492     end
493
494     $modified_pixbufs[thumbnail_img] ||= {}
495     $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
496     msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
497
498     update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
499 end
500
501 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
502     $modified = true
503
504     rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
505
506     save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
507               proc { |angle|
508                   rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
509                   $notebook.set_page(attributes_prefix != '' ? 0 : 1)
510                   proc {
511                       rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
512                       $notebook.set_page(0)
513                       $notebook.set_page(attributes_prefix != '' ? 0 : 1)
514                   }
515               }, -angle)
516 end
517
518 def color_swap(xmldir, attributes_prefix)
519     $modified = true
520     if xmldir.attributes["#{attributes_prefix}color-swap"]
521         xmldir.delete_attribute("#{attributes_prefix}color-swap")
522     else
523         xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
524     end
525 end
526
527 def enhance(xmldir, attributes_prefix)
528     $modified = true
529     if xmldir.attributes["#{attributes_prefix}enhance"]
530         xmldir.delete_attribute("#{attributes_prefix}enhance")
531     else
532         xmldir.add_attribute("#{attributes_prefix}enhance", '1')
533     end
534 end
535
536 def change_seektime(xmldir, attributes_prefix, value)
537     $modified = true
538     xmldir.add_attribute("#{attributes_prefix}seektime", value)
539 end
540
541 def ask_new_seektime(xmldir, attributes_prefix)
542     if xmldir
543         value = xmldir.attributes["#{attributes_prefix}seektime"]
544     else
545         value = ''
546     end
547
548     dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
549                              $main_window,
550                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
551                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
552                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
553
554     lbl = Gtk::Label.new
555     lbl.markup = utf8(
556 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
557 from, in seconds.
558 "))
559     dialog.vbox.add(lbl)
560     dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
561     entry.signal_connect('key-press-event') { |w, event|
562         if event.keyval == Gdk::Keyval::GDK_Return
563             dialog.response(Gtk::Dialog::RESPONSE_OK)
564             true
565         elsif event.keyval == Gdk::Keyval::GDK_Escape
566             dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
567             true
568         else
569             false  #- propagate if needed
570         end
571     }
572
573     dialog.window_position = Gtk::Window::POS_MOUSE
574     dialog.show_all
575
576     dialog.run { |response|
577         newval = entry.text
578         dialog.destroy
579         if response == Gtk::Dialog::RESPONSE_OK
580             $modified = true
581             msg 3, "changing seektime to #{newval}"
582             return { :old => value, :new => newval }
583         else
584             return nil
585         end
586     }
587 end
588
589 def change_pano_amount(xmldir, attributes_prefix, value)
590     $modified = true
591     if value.nil?
592         xmldir.delete_attribute("#{attributes_prefix}pano-amount")
593     else
594         xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
595     end
596 end
597
598 def ask_new_pano_amount(xmldir, attributes_prefix)
599     if xmldir
600         value = xmldir.attributes["#{attributes_prefix}pano-amount"]
601     else
602         value = nil
603     end
604
605     dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
606                              $main_window,
607                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
608                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
609                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
610
611     lbl = Gtk::Label.new
612     lbl.markup = utf8(
613 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
614 of this panorama image compared to other regular images. For example, if the panorama
615 was taken out of four photos on one row, counting the necessary overlap, the width of
616 this panorama image should probably be roughly three times the width of regular images.
617
618 With this information, booh will be able to generate panorama thumbnails looking
619 the right 'size', since the height of the thumbnail for this image will be similar
620 to the height of other thumbnails.
621 "))
622     dialog.vbox.add(lbl)
623     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)")))).
624                                                                          add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
625                                                                          add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
626                                                                          add(Gtk::Label.new(utf8(_("times the width of other images"))))))
627     spin.signal_connect('value-changed') {
628         rb_yes.active = true
629     }
630     dialog.window_position = Gtk::Window::POS_MOUSE
631     dialog.show_all
632     if value
633         spin.value = value.to_f
634         rb_yes.active = true
635         spin.grab_focus
636     else
637         rb_no.active = true
638     end
639
640     dialog.run { |response|
641         if rb_no.active?
642             newval = nil
643         else
644             newval = spin.value.to_f
645         end
646         dialog.destroy
647         if response == Gtk::Dialog::RESPONSE_OK
648             $modified = true
649             msg 3, "changing panorama amount to #{newval}"
650             return { :old => value, :new => newval }
651         else
652             return nil
653         end
654     }
655 end
656
657 def change_whitebalance(xmlelem, attributes_prefix, value)
658     $modified = true
659     xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
660 end
661
662 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
663
664     #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
665     if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
666         save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
667         xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
668         save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
669         xmlelem.delete_attribute("#{attributes_prefix}white-balance")
670         destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
671         gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
672                                 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
673         $modified_pixbufs[thumbnail_img] ||= {}
674         $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
675         xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
676         if save_gammacorrect
677             xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
678             $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
679         end
680         $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
681     end
682
683     $modified_pixbufs[thumbnail_img] ||= {}
684     $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
685
686     update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
687 end
688
689 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
690     #- init $modified_pixbufs correctly
691 #    update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
692
693     value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
694
695     dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
696                              $main_window,
697                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
698                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
699                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
700
701     lbl = Gtk::Label.new
702     lbl.markup = utf8(
703 _("You can fix the <b>white balance</b> of the image, if your image is too blue
704 or too yellow because the recorder didn't detect the light correctly. Drag the
705 slider below the image to the left for more blue, to the right for more yellow.
706 "))
707     dialog.vbox.add(lbl)
708     if img_
709         dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
710     end
711     dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
712     
713     dialog.window_position = Gtk::Window::POS_MOUSE
714     dialog.show_all
715
716     lastval = nil
717     timeout = Gtk.timeout_add(100) {
718         if hs.value != lastval
719             lastval = hs.value
720             if img_
721                 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
722             end
723         end
724         true
725     }
726
727     dialog.run { |response|
728         Gtk.timeout_remove(timeout)
729         if response == Gtk::Dialog::RESPONSE_OK
730             $modified = true
731             newval = hs.value.to_s
732             msg 3, "changing white balance to #{newval}"
733             dialog.destroy
734             return { :old => value, :new => newval }
735         else
736             if thumbnail_img
737                 $modified_pixbufs[thumbnail_img] ||= {}
738                 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
739                 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
740             end
741             dialog.destroy
742             return nil
743         end
744     }
745 end
746
747 def change_gammacorrect(xmlelem, attributes_prefix, value)
748     $modified = true
749     xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
750 end
751
752 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
753
754     #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
755     if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
756         save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
757         xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
758         save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
759         xmlelem.delete_attribute("#{attributes_prefix}white-balance")
760         destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
761         gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
762                                 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
763         $modified_pixbufs[thumbnail_img] ||= {}
764         $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
765         xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
766         if save_whitebalance
767             xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
768             $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
769         end
770         $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
771     end
772
773     $modified_pixbufs[thumbnail_img] ||= {}
774     $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
775
776     update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
777 end
778
779 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
780     #- init $modified_pixbufs correctly
781 #    update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
782
783     value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
784
785     dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
786                              $main_window,
787                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
788                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
789                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
790
791     lbl = Gtk::Label.new
792     lbl.markup = utf8(
793 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
794 or too bright. Drag the slider below the image.
795 "))
796     dialog.vbox.add(lbl)
797     if img_
798         dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
799     end
800     dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
801     
802     dialog.window_position = Gtk::Window::POS_MOUSE
803     dialog.show_all
804
805     lastval = nil
806     timeout = Gtk.timeout_add(100) {
807         if hs.value != lastval
808             lastval = hs.value
809             if img_
810                 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
811             end
812         end
813         true
814     }
815
816     dialog.run { |response|
817         Gtk.timeout_remove(timeout)
818         if response == Gtk::Dialog::RESPONSE_OK
819             $modified = true
820             newval = hs.value.to_s
821             msg 3, "gamma correction to #{newval}"
822             dialog.destroy
823             return { :old => value, :new => newval }
824         else
825             if thumbnail_img
826                 $modified_pixbufs[thumbnail_img] ||= {}
827                 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
828                 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
829             end
830             dialog.destroy
831             return nil
832         end
833     }
834 end
835
836 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
837     if File.exists?(destfile)
838         File.delete(destfile)
839     end
840     #- type can be 'element' or 'subdir'
841     if type == 'element'
842         gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
843     else
844         gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
845     end
846 end
847
848 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
849     Thread.new {
850         push_mousecursor_wait
851         gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
852         gtk_thread_protect {
853             img.set(destfile)
854             $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
855         }
856         pop_mousecursor
857     }
858 end
859
860 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
861     distribute_multiple_call = Proc.new { |action, arg|
862         $selected_elements.each_key { |path|
863             $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
864         }
865         if possible_actions[:can_multiple] && $selected_elements.length > 0
866             UndoHandler.begin_batch
867             $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
868             UndoHandler.end_batch
869         else
870             closures[action].call(arg)
871         end
872         $selected_elements = {}
873     }
874     menu = Gtk::Menu.new
875     if optionals.include?('change_image')
876         menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
877         changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
878         changeimg.signal_connect('activate') { closures[:change].call }
879         menu.append(Gtk::SeparatorMenuItem.new)
880     end
881     if !possible_actions[:can_multiple] || $selected_elements.length == 0
882         if closures[:view]
883             if type == 'image'
884                 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
885                 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
886                 view.signal_connect('activate') { closures[:view].call }
887             else
888                 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
889                 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
890                 view.signal_connect('activate') { closures[:view].call }
891                 menu.append(Gtk::SeparatorMenuItem.new)
892             end
893         end
894         if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
895             menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
896             exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
897             exif.signal_connect('activate') { show_popup($main_window,
898                                                          utf8(`exif -m '#{fullpath}'`),
899                                                          { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
900             menu.append(Gtk::SeparatorMenuItem.new)
901         end
902     end
903     menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
904     r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
905     r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
906     menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
907     r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
908     r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
909     if !possible_actions[:can_multiple] || $selected_elements.length == 0
910         menu.append(Gtk::SeparatorMenuItem.new)
911         if !possible_actions[:forbid_left]
912             menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
913             moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
914             moveleft.signal_connect('activate') { closures[:move].call('left') }
915             if !possible_actions[:can_left]
916                 moveleft.sensitive = false
917             end
918         end
919         if !possible_actions[:forbid_right]
920             menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
921             moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
922             moveright.signal_connect('activate') { closures[:move].call('right') }
923             if !possible_actions[:can_right]
924                 moveright.sensitive = false
925             end
926         end
927         if optionals.include?('move_top')
928             menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
929             movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
930             movetop.signal_connect('activate') { closures[:move].call('top') }
931             if !possible_actions[:can_top]
932                 movetop.sensitive = false
933             end
934         end
935         menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
936         moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
937         moveup.signal_connect('activate') { closures[:move].call('up') }
938         if !possible_actions[:can_up]
939             moveup.sensitive = false
940         end
941         menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
942         movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
943         movedown.signal_connect('activate') { closures[:move].call('down') }
944         if !possible_actions[:can_down]
945             movedown.sensitive = false
946         end
947         if optionals.include?('move_bottom')
948             menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
949             movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
950             movebottom.signal_connect('activate') { closures[:move].call('bottom') }
951             if !possible_actions[:can_bottom]
952                 movebottom.sensitive = false
953             end
954         end
955     end
956     if type == 'video'
957         if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
958             menu.append(Gtk::SeparatorMenuItem.new)
959 #            menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
960 #            color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
961 #            color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
962             menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
963             flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
964             flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
965             menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
966             seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
967             seektime.signal_connect('activate') {
968                 if possible_actions[:can_multiple] && $selected_elements.length > 0
969                     if values = ask_new_seektime(nil, '')
970                         distribute_multiple_call.call(:seektime, values)
971                     end
972                 else
973                     closures[:seektime].call
974                 end
975             }
976         end
977     end
978     menu.append(               Gtk::SeparatorMenuItem.new)
979     menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
980     gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
981     gammacorrect.signal_connect('activate') { 
982         if possible_actions[:can_multiple] && $selected_elements.length > 0
983             if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
984                 distribute_multiple_call.call(:gammacorrect, values)
985             end
986         else
987             closures[:gammacorrect].call
988         end
989     }
990     menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
991     whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
992     whitebalance.signal_connect('activate') { 
993         if possible_actions[:can_multiple] && $selected_elements.length > 0
994             if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
995                 distribute_multiple_call.call(:whitebalance, values)
996             end
997         else
998             closures[:whitebalance].call
999         end
1000     }
1001     if !possible_actions[:can_multiple] || $selected_elements.length == 0
1002         menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
1003                                                                                                              _("Enhance constrast"))))
1004     else
1005         menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
1006     end
1007     enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1008     enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
1009     if type == 'image' && possible_actions[:can_panorama]
1010         menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
1011         panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
1012         panorama.signal_connect('activate') {
1013             if possible_actions[:can_multiple] && $selected_elements.length > 0
1014                 if values = ask_new_pano_amount(nil, '')
1015                     distribute_multiple_call.call(:pano, values)
1016                 end
1017             else
1018                 distribute_multiple_call.call(:pano)
1019             end
1020        }
1021     end
1022     menu.append(               Gtk::SeparatorMenuItem.new)
1023     if optionals.include?('delete')
1024         menu.append(cut_item     = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1025         cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1026         if !possible_actions[:can_multiple] || $selected_elements.length == 0
1027             menu.append(paste_item   = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1028             paste_item.signal_connect('activate') { closures[:paste].call }
1029             menu.append(clear_item   = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1030             clear_item.signal_connect('activate') { $cuts = [] }
1031             if $cuts.size == 0
1032                 paste_item.sensitive = clear_item.sensitive = false
1033             end
1034         end
1035         menu.append(               Gtk::SeparatorMenuItem.new)
1036     end
1037     if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1038         menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1039         editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1040         editexternally.signal_connect('activate') {
1041             if check_image_editor
1042                 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1043                 msg 2, cmd
1044                 system(cmd)
1045             end
1046         }
1047     end
1048     menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1049     refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1050     if optionals.include?('delete')
1051         menu.append(delete_item  = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1052         delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1053     end
1054     menu.show_all
1055     menu.popup(nil, nil, event.button, event.time)
1056 end
1057
1058 def delete_current_subalbum
1059     $modified = true
1060     sel = $albums_tv.selection.selected_rows
1061     $xmldir.elements.each { |e|
1062         if e.name == 'image' || e.name == 'video'
1063             e.add_attribute('deleted', 'true')
1064         end
1065     }
1066     #- branch if we have a non deleted subalbum
1067     if $xmldir.child_byname_notattr('dir', 'deleted')
1068         $xmldir.delete_attribute('thumbnails-caption')
1069         $xmldir.delete_attribute('thumbnails-captionfile')
1070     else
1071         $xmldir.add_attribute('deleted', 'true')
1072         moveup = $xmldir
1073         while moveup.parent.name == 'dir'
1074             moveup = moveup.parent
1075             if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1076                 moveup.add_attribute('deleted', 'true')
1077             else
1078                 break
1079             end
1080         end
1081         sel[0].up!
1082     end
1083     save_changes('forced')
1084     populate_subalbums_treeview(false)
1085     $albums_tv.selection.select_path(sel[0])
1086 end
1087
1088 def restore_deleted
1089     $modified = true
1090     save_changes
1091     $current_path = nil  #- prevent save_changes from being rerun again
1092     sel = $albums_tv.selection.selected_rows
1093     restore_one = proc { |xmldir|
1094         xmldir.elements.each { |e|
1095             if e.name == 'dir' && e.attributes['deleted']
1096                 restore_one.call(e)
1097             end
1098             e.delete_attribute('deleted')
1099         }
1100     }
1101     restore_one.call($xmldir)
1102     populate_subalbums_treeview(false)
1103     $albums_tv.selection.select_path(sel[0])
1104 end
1105
1106 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1107
1108     img = nil
1109     frame1 = Gtk::Frame.new
1110     fullpath = from_utf8("#{$current_path}/#{filename}")
1111
1112     my_gen_real_thumbnail = proc {
1113         gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1114     }
1115
1116     if type == 'video'
1117         pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1118         frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1119                                  pack_start(img = Gtk::Image.new).
1120                                  pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1121         px, mask = pxb.render_pixmap_and_mask(0)
1122         da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1123         da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1124     else
1125         frame1.add(img = Gtk::Image.new)
1126     end
1127
1128     #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1129     if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1130         my_gen_real_thumbnail.call
1131     else
1132         img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1133     end
1134
1135     evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1136
1137     tooltips = Gtk::Tooltips.new
1138     tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1139     tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1140
1141     frame2, textview = create_editzone($autotable_sw, 1, img)
1142     textview.buffer.text = caption
1143     textview.set_justification(Gtk::Justification::CENTER)
1144
1145     vbox = Gtk::VBox.new(false, 5)
1146     vbox.pack_start(evtbox, false, false)
1147     vbox.pack_start(frame2, false, false)
1148     autotable.append(vbox, filename)
1149
1150     #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1151     $vbox2widgets[vbox] = { :textview => textview, :image => img }
1152
1153     #- to be able to find widgets by name
1154     $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1155
1156     cleanup_all_thumbnails = proc {
1157         #- remove out of sync images
1158         dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1159         for sizeobj in $images_size
1160             #- cannot use sizeobj because panoramic images will have a larger width
1161             Dir.glob("#{dest_img_base}-*.jpg") do |file|
1162                 File.delete(file)
1163             end
1164         end
1165
1166     }
1167
1168     refresh = proc {
1169         cleanup_all_thumbnails.call
1170         #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1171         $modified = true
1172         $xmldir.delete_attribute('already-generated')
1173         my_gen_real_thumbnail.call
1174     }
1175  
1176     rotate_and_cleanup = proc { |angle|
1177         cleanup_all_thumbnails.call
1178         rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1179     }
1180
1181     move = proc { |direction|
1182         do_method = "move_#{direction}"
1183         undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1184         perform = proc {
1185             done = autotable.method(do_method).call(vbox)
1186             textview.grab_focus  #- because if moving, focus is stolen
1187             done
1188         }
1189         if perform.call
1190             save_undo(_("move %s") % direction,
1191                       proc {
1192                           autotable.method(undo_method).call(vbox)
1193                           textview.grab_focus  #- because if moving, focus is stolen
1194                           autoscroll_if_needed($autotable_sw, img, textview)
1195                           $notebook.set_page(1)
1196                           proc {
1197                               autotable.method(do_method).call(vbox)
1198                               textview.grab_focus  #- because if moving, focus is stolen
1199                               autoscroll_if_needed($autotable_sw, img, textview)
1200                               $notebook.set_page(1)
1201                           }
1202                       })
1203         end
1204     }
1205
1206     color_swap_and_cleanup = proc {
1207         perform_color_swap_and_cleanup = proc {
1208             cleanup_all_thumbnails.call
1209             color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1210             my_gen_real_thumbnail.call
1211         }
1212
1213         perform_color_swap_and_cleanup.call
1214
1215         save_undo(_("color swap"),
1216                   proc {
1217                       perform_color_swap_and_cleanup.call
1218                       textview.grab_focus
1219                       autoscroll_if_needed($autotable_sw, img, textview)
1220                       $notebook.set_page(1)
1221                       proc {
1222                           perform_color_swap_and_cleanup.call
1223                           textview.grab_focus
1224                           autoscroll_if_needed($autotable_sw, img, textview)
1225                           $notebook.set_page(1)
1226                       }
1227                   })
1228     }
1229
1230     change_seektime_and_cleanup_real = proc { |values|
1231         perform_change_seektime_and_cleanup = proc { |val|
1232             cleanup_all_thumbnails.call
1233             change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1234             my_gen_real_thumbnail.call
1235         }
1236         perform_change_seektime_and_cleanup.call(values[:new])
1237         
1238         save_undo(_("specify seektime"),
1239                   proc {
1240                       perform_change_seektime_and_cleanup.call(values[:old])
1241                       textview.grab_focus
1242                       autoscroll_if_needed($autotable_sw, img, textview)
1243                       $notebook.set_page(1)
1244                       proc {
1245                           perform_change_seektime_and_cleanup.call(values[:new])
1246                           textview.grab_focus
1247                           autoscroll_if_needed($autotable_sw, img, textview)
1248                           $notebook.set_page(1)
1249                       }
1250                   })
1251     }
1252
1253     change_seektime_and_cleanup = proc {
1254         if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1255             change_seektime_and_cleanup_real.call(values)
1256         end
1257     }
1258
1259     change_pano_amount_and_cleanup_real = proc { |values|
1260         perform_change_pano_amount_and_cleanup = proc { |val|
1261             cleanup_all_thumbnails.call
1262             change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1263         }
1264         perform_change_pano_amount_and_cleanup.call(values[:new])
1265         
1266         save_undo(_("change panorama amount"),
1267                   proc {
1268                       perform_change_pano_amount_and_cleanup.call(values[:old])
1269                       textview.grab_focus
1270                       autoscroll_if_needed($autotable_sw, img, textview)
1271                       $notebook.set_page(1)
1272                       proc {
1273                           perform_change_pano_amount_and_cleanup.call(values[:new])
1274                           textview.grab_focus
1275                           autoscroll_if_needed($autotable_sw, img, textview)
1276                           $notebook.set_page(1)
1277                       }
1278                   })
1279     }
1280
1281     change_pano_amount_and_cleanup = proc {
1282         if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1283             change_pano_amount_and_cleanup_real.call(values)
1284         end
1285     }
1286
1287     whitebalance_and_cleanup_real = proc { |values|
1288         perform_change_whitebalance_and_cleanup = proc { |val|
1289             cleanup_all_thumbnails.call
1290             change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1291             recalc_whitebalance(val, fullpath, thumbnail_img, img,
1292                                 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1293         }
1294         perform_change_whitebalance_and_cleanup.call(values[:new])
1295
1296         save_undo(_("fix white balance"),
1297                   proc {
1298                       perform_change_whitebalance_and_cleanup.call(values[:old])
1299                       textview.grab_focus
1300                       autoscroll_if_needed($autotable_sw, img, textview)
1301                       $notebook.set_page(1)
1302                       proc {
1303                           perform_change_whitebalance_and_cleanup.call(values[:new])
1304                           textview.grab_focus
1305                           autoscroll_if_needed($autotable_sw, img, textview)
1306                           $notebook.set_page(1)
1307                       }
1308                   })
1309     }
1310
1311     whitebalance_and_cleanup = proc {
1312         if values = ask_whitebalance(fullpath, thumbnail_img, img,
1313                                      $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1314             whitebalance_and_cleanup_real.call(values)
1315         end
1316     }
1317
1318     gammacorrect_and_cleanup_real = proc { |values|
1319         perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1320             cleanup_all_thumbnails.call
1321             change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1322             recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1323                                 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1324         }
1325         perform_change_gammacorrect_and_cleanup.call(values[:new])
1326         
1327         save_undo(_("gamma correction"),
1328                   Proc.new {
1329                       perform_change_gammacorrect_and_cleanup.call(values[:old])
1330                       textview.grab_focus
1331                       autoscroll_if_needed($autotable_sw, img, textview)
1332                       $notebook.set_page(1)
1333                       Proc.new {
1334                           perform_change_gammacorrect_and_cleanup.call(values[:new])
1335                           textview.grab_focus
1336                           autoscroll_if_needed($autotable_sw, img, textview)
1337                           $notebook.set_page(1)
1338                       }
1339                   })
1340     }
1341     
1342     gammacorrect_and_cleanup = Proc.new {
1343         if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1344                                      $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1345             gammacorrect_and_cleanup_real.call(values)
1346         end
1347     }
1348     
1349     enhance_and_cleanup = proc {
1350         perform_enhance_and_cleanup = proc {
1351             cleanup_all_thumbnails.call
1352             enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1353             my_gen_real_thumbnail.call
1354         }
1355         
1356         cleanup_all_thumbnails.call
1357         perform_enhance_and_cleanup.call
1358
1359         save_undo(_("enhance"),
1360                   proc {
1361                       perform_enhance_and_cleanup.call
1362                       textview.grab_focus
1363                       autoscroll_if_needed($autotable_sw, img, textview)
1364                       $notebook.set_page(1)
1365                       proc {
1366                           perform_enhance_and_cleanup.call
1367                           textview.grab_focus
1368                           autoscroll_if_needed($autotable_sw, img, textview)
1369                           $notebook.set_page(1)
1370                       }
1371                   })
1372     }
1373
1374     delete = proc { |isacut|
1375         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 })
1376             $modified = true
1377             after = nil
1378             perform_delete = proc {
1379                 after = autotable.get_next_widget(vbox)
1380                 if !after
1381                     after = autotable.get_previous_widget(vbox)
1382                 end
1383                 if $config['deleteondisk'] && !isacut
1384                     msg 3, "scheduling for delete: #{fullpath}"
1385                     $todelete << fullpath
1386                 end
1387                 autotable.remove_widget(vbox)
1388                 if after
1389                     $vbox2widgets[after][:textview].grab_focus
1390                     autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1391                 end
1392             }
1393             
1394             previous_pos = autotable.get_current_number(vbox)
1395             perform_delete.call
1396
1397             if !after
1398                 delete_current_subalbum
1399             else
1400                 save_undo(_("delete"),
1401                           proc { |pos|
1402                               autotable.reinsert(pos, vbox, filename)
1403                               $notebook.set_page(1)
1404                               autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1405                               $cuts = []
1406                               msg 3, "removing deletion schedule of: #{fullpath}"
1407                               $todelete.delete(fullpath)  #- unconditional because deleteondisk option could have been modified
1408                               proc {
1409                                   perform_delete.call
1410                                   $notebook.set_page(1)
1411                               }
1412                           }, previous_pos)
1413             end
1414         end
1415     }
1416
1417     cut = proc {
1418         delete.call(true)
1419         $cuts << { :vbox => vbox, :filename => filename }
1420         $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1421     }
1422     paste = proc {
1423         if $cuts.size > 0
1424             $cuts.each { |elem|
1425                 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1426             }
1427             last = $cuts[-1]
1428             autotable.queue_draws << proc {
1429                 $vbox2widgets[last[:vbox]][:textview].grab_focus
1430                 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1431             }
1432             save_undo(_("paste"),
1433                       proc { |cuts|
1434                           cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1435                           $notebook.set_page(1)
1436                           proc {
1437                               cuts.each { |elem|
1438                                   autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1439                               }
1440                               $notebook.set_page(1)
1441                           }
1442                       }, $cuts)
1443             $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1444             $cuts = []
1445         end
1446     }
1447
1448     $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1449                                  :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1450                                  :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1451                                  :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1452
1453     textview.signal_connect('key-press-event') { |w, event|
1454         propagate = true
1455         if event.state != 0
1456             x, y = autotable.get_current_pos(vbox)
1457             control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1458             shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1459             alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1460             if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1461                 if control_pressed
1462                     if widget_up = autotable.get_widget_at_pos(x, y - 1)
1463                         $vbox2widgets[widget_up][:textview].grab_focus
1464                     end
1465                 end
1466                 if shift_pressed
1467                     move.call('up')
1468                 end
1469             end
1470             if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1471                 if control_pressed
1472                     if widget_down = autotable.get_widget_at_pos(x, y + 1)
1473                         $vbox2widgets[widget_down][:textview].grab_focus
1474                     end
1475                 end
1476                 if shift_pressed
1477                     move.call('down')
1478                 end
1479             end
1480             if event.keyval == Gdk::Keyval::GDK_Left
1481                 if x > 0
1482                     if control_pressed
1483                         $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1484                     end
1485                     if shift_pressed
1486                         move.call('left')
1487                     end
1488                 end
1489                 if alt_pressed
1490                     rotate_and_cleanup.call(-90)
1491                 end
1492             end
1493             if event.keyval == Gdk::Keyval::GDK_Right
1494                 next_ = autotable.get_next_widget(vbox)
1495                 if next_ && autotable.get_current_pos(next_)[0] > x
1496                     if control_pressed
1497                         $vbox2widgets[next_][:textview].grab_focus
1498                     end
1499                     if shift_pressed
1500                         move.call('right')
1501                     end
1502                 end
1503                 if alt_pressed
1504                     rotate_and_cleanup.call(90)
1505                 end
1506             end
1507             if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1508                 delete.call(false)
1509             end
1510             if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1511                 view_element(filename, { :delete => delete })
1512                 propagate = false
1513             end
1514             if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1515                 perform_undo
1516             end
1517             if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1518                 perform_redo
1519             end
1520         end
1521         !propagate  #- propagate if needed
1522     }
1523
1524     $ignore_next_release = false
1525     evtbox.signal_connect('button-press-event') { |w, event|
1526         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1527             if event.state & Gdk::Window::BUTTON3_MASK != 0
1528                 #- gesture redo: hold right mouse button then click left mouse button
1529                 $config['nogestures'] or perform_redo
1530                 $ignore_next_release = true
1531             else
1532                 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1533                 if $r90.active?
1534                     rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1535                 elsif $r270.active?
1536                     rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1537                 elsif $enhance.active?
1538                     enhance_and_cleanup.call
1539                 elsif $delete.active?
1540                     delete.call(false)
1541                 else
1542                     textview.grab_focus
1543                     $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1544                 end
1545             end
1546             $button1_pressed_autotable = true
1547         elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1548             if event.state & Gdk::Window::BUTTON1_MASK != 0
1549                 #- gesture undo: hold left mouse button then click right mouse button
1550                 $config['nogestures'] or perform_undo
1551                 $ignore_next_release = true
1552             end
1553         elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1554             view_element(filename, { :delete => delete })
1555         end
1556         false   #- propagate
1557     }
1558
1559     evtbox.signal_connect('button-release-event') { |w, event|
1560         if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1561             if !$ignore_next_release
1562                 x, y = autotable.get_current_pos(vbox)
1563                 next_ = autotable.get_next_widget(vbox)
1564                 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1565                                      { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1566                                        :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1567                                      { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1568                                        :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1569                                        :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1570                                        :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1571             end
1572             $ignore_next_release = false
1573             $gesture_press = nil
1574         end
1575         false   #- propagate
1576     }
1577
1578     #- handle reordering with drag and drop
1579     Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1580     Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1581     vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1582         selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1583     }
1584
1585     vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1586         done = false
1587         #- mouse gesture first (dnd disables button-release-event)
1588         if $gesture_press && $gesture_press[:filename] == filename
1589             if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1590                 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1591                 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1592                 rotate_and_cleanup.call(angle)
1593                 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1594                 done = true
1595             elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1596                 msg 3, "gesture delete: click-drag right button to the bottom"
1597                 delete.call(false)
1598                 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1599                 done = true
1600             end
1601         end
1602         if !done
1603             ctxt.targets.each { |target|
1604                 if target.name == 'reorder-elements'
1605                     move_dnd = proc { |from,to|
1606                         if from != to
1607                             $modified = true
1608                             autotable.move(from, to)
1609                             save_undo(_("reorder"),
1610                                       proc { |from, to|
1611                                           if to > from
1612                                               autotable.move(to - 1, from)
1613                                           else
1614                                               autotable.move(to, from + 1)
1615                                           end
1616                                           $notebook.set_page(1)
1617                                           proc {
1618                                               autotable.move(from, to)
1619                                               $notebook.set_page(1)
1620                                           }
1621                                       }, from, to)
1622                         end
1623                     }
1624                     if $multiple_dnd.size == 0
1625                         move_dnd.call(selection_data.data.to_i,
1626                                       autotable.get_current_number(vbox))
1627                     else
1628                         UndoHandler.begin_batch
1629                         $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1630                                       each { |path|
1631                             #- need to update current position between each call
1632                             move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1633                                           autotable.get_current_number(vbox))
1634                         }
1635                         UndoHandler.end_batch
1636                     end
1637                     $multiple_dnd = []
1638                 end
1639             }
1640         end
1641     }
1642
1643     vbox.show_all
1644 end
1645
1646 def create_auto_table
1647
1648     $autotable = Gtk::AutoTable.new(5)
1649
1650     $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1651     thumbnails_vb = Gtk::VBox.new(false, 5)
1652
1653     frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1654     $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1655     thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1656     thumbnails_vb.add($autotable)
1657
1658     $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1659     $autotable_sw.add_with_viewport(thumbnails_vb)
1660
1661     #- follows stuff for handling multiple elements selection
1662     press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1663     gc = nil
1664     update_selected = proc {
1665         $autotable.current_order.each { |path|
1666             w = $name2widgets[path][:evtbox].window
1667             xm = w.position[0] + w.size[0]/2
1668             ym = w.position[1] + w.size[1]/2
1669             if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1670                 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1671                     $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1672                     $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1673                 end
1674             end
1675             if $selected_elements[path] && ! $selected_elements[path][:keep]
1676                 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))
1677                     $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1678                     $selected_elements.delete(path)
1679                 end
1680             end
1681         }
1682     }
1683     $autotable.signal_connect('realize') { |w,e|
1684         gc = Gdk::GC.new($autotable.window)
1685         gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1686         gc.function = Gdk::GC::INVERT
1687         #- autoscroll handling for DND and multiple selections
1688         Gtk.timeout_add(100) {
1689             if ! $autotable.window.nil?
1690                 w, x, y, mask = $autotable.window.pointer
1691                 if mask & Gdk::Window::BUTTON1_MASK != 0
1692                     if y < $autotable_sw.vadjustment.value
1693                         if pos_x
1694                             $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]])
1695                         end
1696                         if $button1_pressed_autotable || press_x
1697                             scroll_upper($autotable_sw, y)
1698                         end
1699                         if not press_x.nil?
1700                             w, pos_x, pos_y = $autotable.window.pointer
1701                             $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]])
1702                             update_selected.call
1703                         end
1704                     end
1705                     if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1706                         if pos_x
1707                             $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]])
1708                         end
1709                         if $button1_pressed_autotable || press_x
1710                             scroll_lower($autotable_sw, y)
1711                         end
1712                         if not press_x.nil?
1713                             w, pos_x, pos_y = $autotable.window.pointer
1714                             $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]])
1715                             update_selected.call
1716                         end
1717                     end
1718                 end
1719             end
1720             ! $autotable.window.nil?
1721         }
1722     }
1723
1724     $autotable.signal_connect('button-press-event') { |w,e|
1725         if e.button == 1
1726             if !$button1_pressed_autotable
1727                 press_x = e.x
1728                 press_y = e.y
1729                 if e.state & Gdk::Window::SHIFT_MASK == 0
1730                     $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1731                     $selected_elements = {}
1732                     $statusbar.push(0, utf8(_("Nothing selected.")))
1733                 else
1734                     $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1735                 end
1736                 set_mousecursor(Gdk::Cursor::TCROSS)
1737             end
1738         end
1739     }
1740     $autotable.signal_connect('button-release-event') { |w,e|
1741         if e.button == 1
1742             if $button1_pressed_autotable
1743                 #- unselect all only now
1744                 $multiple_dnd = $selected_elements.keys
1745                 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1746                 $selected_elements = {}
1747                 $button1_pressed_autotable = false
1748             else
1749                 if pos_x
1750                     $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]])
1751                     if $selected_elements.length > 0
1752                         $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1753                     end
1754                 end
1755                 press_x = press_y = pos_x = pos_y = nil
1756                 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1757             end
1758         end
1759     }
1760     $autotable.signal_connect('motion-notify-event') { |w,e|
1761         if ! press_x.nil?
1762             if pos_x
1763                 $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]])
1764             end
1765             pos_x = e.x
1766             pos_y = e.y
1767             $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]])
1768             update_selected.call
1769         end
1770     }
1771
1772 end
1773
1774 def create_subalbums_page
1775
1776     subalbums_hb = Gtk::HBox.new
1777     $subalbums_vb = Gtk::VBox.new(false, 5)
1778     subalbums_hb.pack_start($subalbums_vb, false, false)
1779     $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1780     $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1781     $subalbums_sw.add_with_viewport(subalbums_hb)
1782 end
1783
1784 def save_current_file
1785     save_changes
1786
1787     if $filename
1788         begin
1789             begin
1790                 ios = File.open($filename, "w")
1791                 $xmldoc.write(ios, 0)
1792                 ios.close
1793             rescue Iconv::IllegalSequence
1794                 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1795                 if ! ios.nil? && ! ios.closed?
1796                     ios.close
1797                 end
1798                 $xmldoc.xml_decl.encoding = 'UTF-8'
1799                 ios = File.open($filename, "w")
1800                 $xmldoc.write(ios, 0)
1801                 ios.close
1802             end
1803             return true
1804         rescue Exception
1805             puts $!
1806             return false
1807         end
1808     end
1809 end
1810
1811 def save_current_file_user
1812     save_tempfilename = $filename
1813     $filename = $orig_filename
1814     if ! save_current_file
1815         show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1816         $filename = save_tempfilename
1817         return
1818     end
1819     $modified = false
1820     $generated_outofline = false
1821     $filename = save_tempfilename
1822
1823     msg 3, "performing actual deletion of: " + $todelete.join(', ')
1824     $todelete.each { |f|
1825         File.delete(f)
1826     }
1827 end
1828
1829 def mark_document_as_dirty
1830     $xmldoc.elements.each('//dir') { |elem|
1831         elem.delete_attribute('already-generated')
1832     }
1833 end
1834
1835 #- ret: true => ok  false => cancel
1836 def ask_save_modifications(msg1, msg2, *options)
1837     ret = true
1838     options = options.size > 0 ? options[0] : {}
1839     if $modified
1840         if options[:disallow_cancel]
1841             dialog = Gtk::Dialog.new(msg1,
1842                                      $main_window,
1843                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1844                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1845                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1846         else
1847             dialog = Gtk::Dialog.new(msg1,
1848                                      $main_window,
1849                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1850                                      [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1851                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1852                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1853         end
1854         dialog.default_response = Gtk::Dialog::RESPONSE_YES
1855         dialog.vbox.add(Gtk::Label.new(msg2))
1856         dialog.window_position = Gtk::Window::POS_CENTER
1857         dialog.show_all
1858         
1859         dialog.run { |response|
1860             dialog.destroy
1861             if response == Gtk::Dialog::RESPONSE_YES
1862                 if ! save_current_file_user
1863                     return ask_save_modifications(msg1, msg2, options)
1864                 end
1865             else
1866                 #- if we have generated an album but won't save modifications, we must remove 
1867                 #- already-generated markers in original file
1868                 if $generated_outofline
1869                     begin
1870                         $xmldoc = Synchronizator.new(REXML::Document.new(File.new($orig_filename)), $xmlaccesslock)
1871                         mark_document_as_dirty
1872                         ios = File.open($orig_filename, "w")
1873                         $xmldoc.write(ios, 0)
1874                         ios.close
1875                     rescue Exception
1876                         puts "exception: #{$!}"
1877                     end
1878                 end
1879             end
1880             if response == Gtk::Dialog::RESPONSE_CANCEL
1881                 ret = false
1882             end
1883             $todelete = []  #- unconditionally clear the list of images/videos to delete
1884         }
1885     end
1886     return ret
1887 end
1888
1889 def try_quit(*options)
1890     if ask_save_modifications(utf8(_("Save before quitting?")),
1891                               utf8(_("Do you want to save your changes before quitting?")),
1892                               *options)
1893         Gtk.main_quit
1894     end
1895 end
1896
1897 def show_popup(parent, msg, *options)
1898     dialog = Gtk::Dialog.new
1899     if options[0] && options[0][:title]
1900         dialog.title = options[0][:title]
1901     else
1902         dialog.title = utf8(_("Booh message"))
1903     end
1904     lbl = Gtk::Label.new
1905     if options[0] && options[0][:nomarkup]
1906         lbl.text = msg
1907     else
1908         lbl.markup = msg
1909     end
1910     if options[0] && options[0][:centered]
1911         lbl.set_justify(Gtk::Justification::CENTER)
1912     end
1913     if options[0] && options[0][:selectable]
1914         lbl.selectable = true
1915     end
1916     if options[0] && options[0][:topwidget]
1917         dialog.vbox.add(options[0][:topwidget])
1918     end
1919     if options[0] && options[0][:scrolled]
1920         sw = Gtk::ScrolledWindow.new(nil, nil)
1921         sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1922         sw.add_with_viewport(lbl)
1923         dialog.vbox.add(sw)
1924         dialog.set_default_size(500, 600)
1925     else
1926         dialog.vbox.add(lbl)
1927         dialog.set_default_size(200, 120)
1928     end
1929     if options[0] && options[0][:okcancel]
1930         dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1931     end
1932     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1933
1934     if options[0] && options[0][:pos_centered]
1935         dialog.window_position = Gtk::Window::POS_CENTER
1936     else
1937         dialog.window_position = Gtk::Window::POS_MOUSE
1938     end
1939
1940     if options[0] && options[0][:linkurl]
1941         linkbut = Gtk::Button.new('')
1942         linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1943         linkbut.signal_connect('clicked') {
1944             open_url(options[0][:linkurl])
1945             dialog.response(Gtk::Dialog::RESPONSE_OK)
1946             set_mousecursor_normal
1947         }
1948         linkbut.relief = Gtk::RELIEF_NONE
1949         linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1950         linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1951         dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1952     end
1953
1954     dialog.show_all
1955
1956     if !options[0] || !options[0][:not_transient]
1957         dialog.transient_for = parent
1958         dialog.run { |response|
1959             dialog.destroy
1960             if options[0] && options[0][:okcancel]
1961                 return response == Gtk::Dialog::RESPONSE_OK
1962             end
1963         }
1964     else
1965         dialog.signal_connect('response') { dialog.destroy }
1966     end
1967 end
1968
1969 def set_mainwindow_title(progress)
1970     filename = $orig_filename || $filename
1971     if progress
1972         if filename
1973             $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
1974         else
1975             $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
1976         end
1977     else
1978         if filename
1979             $main_window.title = 'booh - ' + File.basename(filename)
1980         else
1981             $main_window.title = 'booh'
1982         end
1983     end
1984 end
1985
1986 def backend_wait_message(parent, msg, infopipe_path, mode)
1987     w = create_window
1988     w.set_transient_for(parent)
1989     w.modal = true
1990
1991     vb = Gtk::VBox.new(false, 5).set_border_width(5)
1992     vb.pack_start(Gtk::Label.new(msg), false, false)
1993
1994     vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1995     vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
1996     if mode != 'one dir scan'
1997         vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1998     end
1999     if mode == 'web-album'
2000         vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2001         vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2002     end
2003     vb.pack_start(Gtk::HSeparator.new, false, false)
2004
2005     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2006     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2007     vb.pack_end(bottom, false, false)
2008
2009     directories = nil
2010     update_progression_title_pb1 = proc {
2011         if mode == 'web-album'
2012             set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2013         elsif mode != 'one dir scan'
2014             set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2015         else
2016             set_mainwindow_title(pb1_1.fraction)
2017         end
2018     }
2019
2020     infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2021     refresh_thread = Thread.new {
2022         directories_counter = 0
2023         while line = infopipe.gets
2024             if line =~ /^directories: (\d+), sizes: (\d+)/
2025                 directories = $1.to_f + 1
2026                 sizes = $2.to_f
2027             elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2028                 elements = $3.to_f + 1
2029                 if mode == 'web-album'
2030                     elements += sizes
2031                 end
2032                 element_counter = 0
2033                 gtk_thread_protect { pb1_1.fraction = 0 }
2034                 if mode != 'one dir scan'
2035                     newtext = utf8(full_src_dir_to_rel($1, $2))
2036                     newtext = '/' if newtext == ''
2037                     gtk_thread_protect { pb1_2.text = newtext }
2038                     directories_counter += 1
2039                     gtk_thread_protect {
2040                         pb1_2.fraction = directories_counter / directories
2041                         update_progression_title_pb1.call
2042                     }
2043                 end
2044             elsif line =~ /^processing element$/
2045                 element_counter += 1
2046                 gtk_thread_protect {
2047                     pb1_1.fraction = element_counter / elements
2048                     update_progression_title_pb1.call
2049                 }
2050             elsif line =~ /^processing size$/
2051                 element_counter += 1
2052                 gtk_thread_protect {
2053                     pb1_1.fraction = element_counter / elements
2054                     update_progression_title_pb1.call
2055                 }
2056             elsif line =~ /^finished processing sizes$/
2057                 gtk_thread_protect { pb1_1.fraction = 1 }
2058             elsif line =~ /^creating index.html$/
2059                 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2060                 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2061                 directories_counter = 0
2062             elsif line =~ /^index.html: (.+)\|(.+)/
2063                 newtext = utf8(full_src_dir_to_rel($1, $2))
2064                 newtext = '/' if newtext == ''
2065                 gtk_thread_protect { pb2.text = newtext }
2066                 directories_counter += 1
2067                 gtk_thread_protect {
2068                     pb2.fraction = directories_counter / directories
2069                     set_mainwindow_title(0.9 + pb2.fraction / 10)
2070                 }
2071             elsif line =~ /^die: (.*)$/
2072                 $diemsg = $1
2073             end
2074         end
2075     }
2076
2077     w.add(vb)
2078     w.signal_connect('delete-event') { w.destroy }
2079     w.signal_connect('destroy') {
2080         Thread.kill(refresh_thread)
2081         gtk_thread_flush  #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2082         if infopipe_path
2083             infopipe.close
2084             File.delete(infopipe_path)
2085         end
2086         set_mainwindow_title(nil)
2087     }
2088     w.window_position = Gtk::Window::POS_CENTER
2089     w.show_all
2090
2091     return [ b, w ]
2092 end
2093
2094 def call_backend(cmd, waitmsg, mode, params)
2095     pipe = Tempfile.new("boohpipe")
2096     Thread.critical = true
2097     path = pipe.path
2098     pipe.close!
2099     system("mkfifo #{path}")
2100     Thread.critical = false
2101     cmd += " --info-pipe #{path}"
2102     button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2103     pid = nil
2104     Thread.new {
2105         msg 2, cmd
2106         if pid = fork
2107             id, exitstatus = Process.waitpid2(pid)
2108             gtk_thread_protect { w8.destroy }
2109             if exitstatus == 0
2110                 if params[:successmsg]
2111                     gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2112                 end
2113                 if params[:closure_after]
2114                     gtk_thread_protect(&params[:closure_after])
2115                 end
2116             elsif exitstatus == 15
2117                 #- say nothing, user aborted
2118             else
2119                 gtk_thread_protect { show_popup($main_window,
2120                                                 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2121             end
2122         else
2123             exec(cmd)
2124         end
2125     }
2126     button.signal_connect('clicked') {
2127         Process.kill('SIGTERM', pid)
2128     }
2129 end
2130
2131 def save_changes(*forced)
2132     if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2133         return
2134     end
2135
2136     $xmldir.delete_attribute('already-generated')
2137
2138     propagate_children = proc { |xmldir|
2139         if xmldir.attributes['subdirs-caption']
2140             xmldir.delete_attribute('already-generated')
2141         end
2142         xmldir.elements.each('dir') { |element|
2143             propagate_children.call(element)
2144         }
2145     }
2146
2147     if $xmldir.child_byname_notattr('dir', 'deleted')
2148         new_title = $subalbums_title.buffer.text
2149         if new_title != $xmldir.attributes['subdirs-caption']
2150             parent = $xmldir.parent
2151             if parent.name == 'dir'
2152                 parent.delete_attribute('already-generated')
2153             end
2154             propagate_children.call($xmldir)
2155         end
2156         $xmldir.add_attribute('subdirs-caption', new_title)
2157         $xmldir.elements.each('dir') { |element|
2158             if !element.attributes['deleted']
2159                 path = element.attributes['path']
2160                 newtext = $subalbums_edits[path][:editzone].buffer.text
2161                 if element.attributes['subdirs-caption']
2162                     if element.attributes['subdirs-caption'] != newtext
2163                         propagate_children.call(element)
2164                     end
2165                     element.add_attribute('subdirs-caption',     newtext)
2166                     element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2167                 else
2168                     if element.attributes['thumbnails-caption'] != newtext
2169                         element.delete_attribute('already-generated')
2170                     end
2171                     element.add_attribute('thumbnails-caption',     newtext)
2172                     element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2173                 end
2174             end
2175         }
2176     end
2177
2178     if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2179         if $xmldir.attributes['thumbnails-caption']
2180             path = $xmldir.attributes['path']
2181             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2182         end
2183     elsif $xmldir.attributes['thumbnails-caption']
2184         $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2185     end
2186
2187     if $xmldir.attributes['thumbnails-caption']
2188         if edit = $subalbums_edits[$xmldir.attributes['path']]
2189             $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2190         end
2191     end
2192
2193     #- remove and reinsert elements to reflect new ordering
2194     saves = {}
2195     cpt = 0
2196     $xmldir.elements.each { |element|
2197         if element.name == 'image' || element.name == 'video'
2198             saves[element.attributes['filename']] = element.remove
2199             cpt += 1
2200         end
2201     }
2202     $autotable.current_order.each { |path|
2203         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2204         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2205         saves.delete(path)
2206     }
2207     saves.each_key { |path|
2208         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2209         chld.add_attribute('deleted', 'true')
2210     }
2211 end
2212
2213 def sort_by_exif_date
2214     $modified = true
2215     save_changes
2216     current_order = []
2217     $xmldir.elements.each { |element|
2218         if element.name == 'image' || element.name == 'video'
2219             current_order << element.attributes['filename']
2220         end
2221     }
2222
2223     #- look for EXIF dates
2224     dates = {}
2225
2226     if current_order.size > 20
2227         w = create_window
2228         w.set_transient_for($main_window)
2229         w.modal = true
2230         vb = Gtk::VBox.new(false, 5).set_border_width(5)
2231         vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2232         vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2233         bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2234         b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2235         vb.pack_end(bottom, false, false)
2236         w.add(vb)
2237         w.signal_connect('delete-event') { w.destroy }
2238         w.window_position = Gtk::Window::POS_CENTER
2239         w.show_all
2240
2241         aborted = false
2242         b.signal_connect('clicked') { aborted = true }
2243         i = 0
2244         current_order.each { |f|
2245             i += 1
2246             if entry2type(f) == 'image'
2247                 pb.text = f
2248                 pb.fraction = i.to_f / current_order.size
2249                 Gtk.main_iteration while Gtk.events_pending?
2250                 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2251                 if ! date_time.nil?
2252                     dates[f] = date_time
2253                 end
2254             end
2255             if aborted
2256                 break
2257             end
2258         }
2259         w.destroy
2260         if aborted
2261             return
2262         end
2263
2264     else
2265         current_order.each { |f|
2266             date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2267             if ! date_time.nil?
2268                 dates[f] = date_time
2269             end
2270         }
2271     end
2272
2273     saves = {}
2274     $xmldir.elements.each { |element|
2275         if element.name == 'image' || element.name == 'video'
2276             saves[element.attributes['filename']] = element.remove
2277         end
2278     }
2279
2280     neworder = smartsort(current_order, dates)
2281
2282     neworder.each { |f|
2283         $xmldir.add_element(saves[f].name, saves[f].attributes)
2284     }
2285
2286     #- let the auto-table reflect new ordering
2287     change_dir
2288 end
2289
2290 def remove_all_captions
2291     $modified = true
2292     texts = {}
2293     $autotable.current_order.each { |path|
2294         texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2295         $name2widgets[File.basename(path)][:textview].buffer.text = ''
2296     }
2297     save_undo(_("remove all captions"),
2298               proc { |texts|
2299                   texts.each_key { |key|
2300                       $name2widgets[key][:textview].buffer.text = texts[key]
2301                   }
2302                   $notebook.set_page(1)
2303                   proc {
2304                       texts.each_key { |key|
2305                           $name2widgets[key][:textview].buffer.text = ''
2306                       }
2307                       $notebook.set_page(1)
2308                   }
2309               }, texts)
2310 end
2311
2312 def change_dir
2313     $selected_elements.each_key { |path|
2314         $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2315     }
2316     $autotable.clear
2317     $vbox2widgets = {}
2318     $name2widgets = {}
2319     $name2closures = {}
2320     $selected_elements = {}
2321     $cuts = []
2322     $multiple_dnd = []
2323     UndoHandler.cleanup
2324     $undo_tb.sensitive = $undo_mb.sensitive = false
2325     $redo_tb.sensitive = $redo_mb.sensitive = false
2326
2327     if !$current_path
2328         return
2329     end
2330
2331     $subalbums_vb.children.each { |chld|
2332         $subalbums_vb.remove(chld)
2333     }
2334     $subalbums = Gtk::Table.new(0, 0, true)
2335     current_y_sub_albums = 0
2336
2337     $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"], $xmlaccesslock)
2338     $subalbums_edits = {}
2339     subalbums_counter = 0
2340     subalbums_edits_bypos = {}
2341
2342     add_subalbum = proc { |xmldir, counter|
2343         $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2344         subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2345         if xmldir == $xmldir
2346             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2347             captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2348             caption = xmldir.attributes['thumbnails-caption']
2349             infotype = 'thumbnails'
2350         else
2351             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2352             captionfile, caption = find_subalbum_caption_info(xmldir)
2353             infotype = find_subalbum_info_type(xmldir)
2354         end
2355         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2356         hbox = Gtk::HBox.new
2357         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2358         f = Gtk::Frame.new
2359         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2360
2361         img = nil
2362         my_gen_real_thumbnail = proc {
2363             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2364         }
2365
2366         if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2367             f.add(img = Gtk::Image.new)
2368             my_gen_real_thumbnail.call
2369         else
2370             f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2371         end
2372         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2373         $subalbums.attach(hbox,
2374                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2375
2376         frame, textview = create_editzone($subalbums_sw, 0, img)
2377         textview.buffer.text = caption
2378         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2379                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2380
2381         change_image = proc {
2382             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2383                                             nil,
2384                                             Gtk::FileChooser::ACTION_OPEN,
2385                                             nil,
2386                                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2387             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2388             fc.transient_for = $main_window
2389             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))
2390             f.add(preview_img = Gtk::Image.new)
2391             preview.show_all
2392             fc.signal_connect('update-preview') { |w|
2393                 begin
2394                     if fc.preview_filename
2395                         preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2396                         fc.preview_widget_active = true
2397                     end
2398                 rescue Gdk::PixbufError
2399                     fc.preview_widget_active = false
2400                 end
2401             }
2402             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2403                 $modified = true
2404                 old_file = captionfile
2405                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2406                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2407                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2408                 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2409
2410                 new_file = fc.filename
2411                 msg 3, "new captionfile is: #{fc.filename}"
2412                 perform_changefile = proc {
2413                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2414                     $modified_pixbufs.delete(thumbnail_file)
2415                     xmldir.delete_attribute("#{infotype}-rotate")
2416                     xmldir.delete_attribute("#{infotype}-color-swap")
2417                     xmldir.delete_attribute("#{infotype}-enhance")
2418                     xmldir.delete_attribute("#{infotype}-seektime")
2419                     my_gen_real_thumbnail.call
2420                 }
2421                 perform_changefile.call
2422
2423                 save_undo(_("change caption file for sub-album"),
2424                           proc {
2425                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2426                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2427                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2428                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2429                               xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2430                               my_gen_real_thumbnail.call
2431                               $notebook.set_page(0)
2432                               proc {
2433                                   perform_changefile.call
2434                                   $notebook.set_page(0)
2435                               }
2436                           })
2437             end
2438             fc.destroy
2439         }
2440
2441         refresh = proc {
2442             if File.exists?(thumbnail_file)
2443                 File.delete(thumbnail_file)
2444             end
2445             my_gen_real_thumbnail.call
2446         }
2447
2448         rotate_and_cleanup = proc { |angle|
2449             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2450             if File.exists?(thumbnail_file)
2451                 File.delete(thumbnail_file)
2452             end
2453         }
2454
2455         move = proc { |direction|
2456             $modified = true
2457
2458             save_changes('forced')
2459             oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2460             if direction == 'up'
2461                 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2462                 subalbums_edits_bypos[oldpos - 1][:position] += 1
2463             end
2464             if direction == 'down'
2465                 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2466                 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2467             end
2468             if direction == 'top'
2469                 for i in 1 .. oldpos - 1
2470                     subalbums_edits_bypos[i][:position] += 1
2471                 end
2472                 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2473             end
2474             if direction == 'bottom'
2475                 for i in oldpos + 1 .. subalbums_counter
2476                     subalbums_edits_bypos[i][:position] -= 1
2477                 end
2478                 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2479             end
2480
2481             elems = []
2482             $xmldir.elements.each('dir') { |element|
2483                 if (!element.attributes['deleted'])
2484                     elems << [ element.attributes['path'], element.remove ]
2485                 end
2486             }
2487             elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2488                   each { |e| $xmldir.add_element(e[1]) }
2489             #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2490             $xmldir.elements.each('descendant::dir') { |elem|
2491                 elem.delete_attribute('already-generated')
2492             }
2493
2494             sel = $albums_tv.selection.selected_rows
2495             change_dir
2496             populate_subalbums_treeview(false)
2497             $albums_tv.selection.select_path(sel[0])
2498         }
2499
2500         color_swap_and_cleanup = proc {
2501             perform_color_swap_and_cleanup = proc {
2502                 color_swap(xmldir, "#{infotype}-")
2503                 my_gen_real_thumbnail.call
2504             }
2505             perform_color_swap_and_cleanup.call
2506
2507             save_undo(_("color swap"),
2508                       proc {
2509                           perform_color_swap_and_cleanup.call
2510                           $notebook.set_page(0)
2511                           proc {
2512                               perform_color_swap_and_cleanup.call
2513                               $notebook.set_page(0)
2514                           }
2515                       })
2516         }
2517
2518         change_seektime_and_cleanup = proc {
2519             if values = ask_new_seektime(xmldir, "#{infotype}-")
2520                 perform_change_seektime_and_cleanup = proc { |val|
2521                     change_seektime(xmldir, "#{infotype}-", val)
2522                     my_gen_real_thumbnail.call
2523                 }
2524                 perform_change_seektime_and_cleanup.call(values[:new])
2525
2526                 save_undo(_("specify seektime"),
2527                           proc {
2528                               perform_change_seektime_and_cleanup.call(values[:old])
2529                               $notebook.set_page(0)
2530                               proc {
2531                                   perform_change_seektime_and_cleanup.call(values[:new])
2532                                   $notebook.set_page(0)
2533                               }
2534                           })
2535             end
2536         }
2537
2538         whitebalance_and_cleanup = proc {
2539             if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2540                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2541                 perform_change_whitebalance_and_cleanup = proc { |val|
2542                     change_whitebalance(xmldir, "#{infotype}-", val)
2543                     recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2544                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2545                     if File.exists?(thumbnail_file)
2546                         File.delete(thumbnail_file)
2547                     end
2548                 }
2549                 perform_change_whitebalance_and_cleanup.call(values[:new])
2550                 
2551                 save_undo(_("fix white balance"),
2552                           proc {
2553                               perform_change_whitebalance_and_cleanup.call(values[:old])
2554                               $notebook.set_page(0)
2555                               proc {
2556                                   perform_change_whitebalance_and_cleanup.call(values[:new])
2557                                   $notebook.set_page(0)
2558                               }
2559                           })
2560             end
2561         }
2562
2563         gammacorrect_and_cleanup = proc {
2564             if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2565                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2566                 perform_change_gammacorrect_and_cleanup = proc { |val|
2567                     change_gammacorrect(xmldir, "#{infotype}-", val)
2568                     recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2569                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2570                     if File.exists?(thumbnail_file)
2571                         File.delete(thumbnail_file)
2572                     end
2573                 }
2574                 perform_change_gammacorrect_and_cleanup.call(values[:new])
2575                 
2576                 save_undo(_("gamma correction"),
2577                           proc {
2578                               perform_change_gammacorrect_and_cleanup.call(values[:old])
2579                               $notebook.set_page(0)
2580                               proc {
2581                                   perform_change_gammacorrect_and_cleanup.call(values[:new])
2582                                   $notebook.set_page(0)
2583                               }
2584                           })
2585             end
2586         }
2587
2588         enhance_and_cleanup = proc {
2589             perform_enhance_and_cleanup = proc {
2590                 enhance(xmldir, "#{infotype}-")
2591                 my_gen_real_thumbnail.call
2592             }
2593             
2594             perform_enhance_and_cleanup.call
2595             
2596             save_undo(_("enhance"),
2597                       proc {
2598                           perform_enhance_and_cleanup.call
2599                           $notebook.set_page(0)
2600                           proc {
2601                               perform_enhance_and_cleanup.call
2602                               $notebook.set_page(0)
2603                           }
2604                       })
2605         }
2606
2607         evtbox.signal_connect('button-press-event') { |w, event|
2608             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2609                 if $r90.active?
2610                     rotate_and_cleanup.call(90)
2611                 elsif $r270.active?
2612                     rotate_and_cleanup.call(-90)
2613                 elsif $enhance.active?
2614                     enhance_and_cleanup.call
2615                 end
2616             end
2617             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2618                 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2619                                      { :forbid_left => true, :forbid_right => true,
2620                                        :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2621                                        :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2622                                      { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2623                                        :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2624                                        :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2625             end
2626             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2627                 change_image.call
2628                 true   #- handled
2629             end
2630         }
2631         evtbox.signal_connect('button-press-event') { |w, event|
2632             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2633             false
2634         }
2635
2636         evtbox.signal_connect('button-release-event') { |w, event|
2637             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2638                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2639                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2640                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2641                     msg 3, "gesture rotate: #{angle}"
2642                     rotate_and_cleanup.call(angle)
2643                 end
2644             end
2645             $gesture_press = nil
2646         }
2647                 
2648         $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2649         $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2650         current_y_sub_albums += 1
2651     }
2652
2653     if $xmldir.child_byname_notattr('dir', 'deleted')
2654         #- title edition
2655         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2656         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2657         $subalbums_title.set_justification(Gtk::Justification::CENTER)
2658         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2659         #- this album image/caption
2660         if $xmldir.attributes['thumbnails-caption']
2661             add_subalbum.call($xmldir, 0)
2662         end
2663     end
2664     total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2665     $xmldir.elements.each { |element|
2666         if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2667             #- element (image or video) of this album
2668             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2669             msg 3, "dest_img: #{dest_img}"
2670             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2671             total[element.name] += 1
2672         end
2673         if element.name == 'dir' && !element.attributes['deleted']
2674             #- sub-album image/caption
2675             add_subalbum.call(element, subalbums_counter += 1)
2676             total[element.name] += 1
2677         end
2678     }
2679     $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2680                                                                                 total['image'], total['video'], total['dir'] ]))
2681     $subalbums_vb.add($subalbums)
2682     $subalbums_vb.show_all
2683
2684     if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2685         $notebook.get_tab_label($autotable_sw).sensitive = false
2686         $notebook.set_page(0)
2687         $thumbnails_title.buffer.text = ''
2688     else
2689         $notebook.get_tab_label($autotable_sw).sensitive = true
2690         $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2691     end
2692
2693     if !$xmldir.child_byname_notattr('dir', 'deleted')
2694         $notebook.get_tab_label($subalbums_sw).sensitive = false
2695         $notebook.set_page(1)
2696     else
2697         $notebook.get_tab_label($subalbums_sw).sensitive = true
2698     end
2699 end
2700
2701 def pixbuf_or_nil(filename)
2702     begin
2703         return Gdk::Pixbuf.new(filename)
2704     rescue
2705         return nil
2706     end
2707 end
2708
2709 def theme_choose(current)
2710     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2711                              $main_window,
2712                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2713                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2714                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2715
2716     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2717     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2718     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2719     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2720     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2721     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2722     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2723     treeview.signal_connect('button-press-event') { |w, event|
2724         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2725             dialog.response(Gtk::Dialog::RESPONSE_OK)
2726         end
2727     }
2728
2729     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2730
2731     ([ $FPATH + '/themes/simple' ] + (`find '#{$FPATH}/themes' ~/.booh-themes -mindepth 1 -maxdepth 1 -type d 2>/dev/null`.find_all { |e| e !~ /simple$/ }.sort)).each { |dir|
2732         dir.chomp!
2733         iter = model.append
2734         iter[0] = File.basename(dir)
2735         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2736         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2737         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2738         if File.basename(dir) == current
2739             treeview.selection.select_iter(iter)
2740         end
2741     }
2742     dialog.set_default_size(-1, 500)
2743     dialog.vbox.show_all
2744
2745     dialog.run { |response|
2746         iter = treeview.selection.selected
2747         dialog.destroy
2748         if response == Gtk::Dialog::RESPONSE_OK && iter
2749             return model.get_value(iter, 0)
2750         end
2751     }
2752     return nil
2753 end
2754
2755 def show_password_protections
2756     examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2757         child_iter = $albums_iters[xmldir.attributes['path']]
2758         if xmldir.attributes['password-protect']
2759             child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2760             already_protected = true
2761         elsif already_protected
2762             pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2763             if pix
2764                 pix = pix.saturate_and_pixelate(1, true)
2765             end
2766             child_iter[2] = pix
2767         else
2768             child_iter[2] = nil
2769         end
2770         xmldir.elements.each('dir') { |elem|
2771             if !elem.attributes['deleted']
2772                 examine_dir_elem.call(child_iter, elem, already_protected)
2773             end
2774         }
2775     }
2776     examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2777 end
2778
2779 def populate_subalbums_treeview(select_first)
2780     $albums_ts.clear
2781     $autotable.clear
2782     $albums_iters = {}
2783     $subalbums_vb.children.each { |chld|
2784         $subalbums_vb.remove(chld)
2785     }
2786
2787     source = $xmldoc.root.attributes['source']
2788     msg 3, "source: #{source}"
2789
2790     xmldir = $xmldoc.elements['//dir']
2791     if !xmldir || xmldir.attributes['path'] != source
2792         msg 1, _("Corrupted booh file...")
2793         return
2794     end
2795
2796     append_dir_elem = proc { |parent_iter, xmldir|
2797         child_iter = $albums_ts.append(parent_iter)
2798         child_iter[0] = File.basename(xmldir.attributes['path'])
2799         child_iter[1] = xmldir.attributes['path']
2800         $albums_iters[xmldir.attributes['path']] = child_iter
2801         msg 3, "puttin location: #{xmldir.attributes['path']}"
2802         xmldir.elements.each('dir') { |elem|
2803             if !elem.attributes['deleted']
2804                 append_dir_elem.call(child_iter, elem)
2805             end
2806         }
2807     }
2808     append_dir_elem.call(nil, xmldir)
2809     show_password_protections
2810
2811     $albums_tv.expand_all
2812     if select_first
2813         $albums_tv.selection.select_iter($albums_ts.iter_first)
2814     end
2815 end
2816
2817 def select_current_theme
2818     select_theme($xmldoc.root.attributes['theme'],
2819                  $xmldoc.root.attributes['limit-sizes'],
2820                  !$xmldoc.root.attributes['optimize-for-32'].nil?,
2821                  $xmldoc.root.attributes['thumbnails-per-row'])
2822 end
2823
2824 def open_file(filename)
2825
2826     $filename = nil
2827     $modified = false
2828     $current_path = nil   #- invalidate
2829     $modified_pixbufs = {}
2830     $albums_ts.clear
2831     $autotable.clear
2832     $subalbums_vb.children.each { |chld|
2833         $subalbums_vb.remove(chld)
2834     }
2835
2836     if !File.exists?(filename)
2837         return utf8(_("File not found."))
2838     end
2839
2840     begin
2841         $xmldoc = Synchronizator.new(REXML::Document.new(File.new(filename)), $xmlaccesslock)
2842     rescue Exception
2843         $xmldoc = nil
2844     end
2845
2846     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2847         if entry2type(filename).nil?
2848             return utf8(_("Not a booh file!"))
2849         else
2850             return utf8(_("Not a booh file!\n\nHint: you cannot import directly a photo or video with File/Open.\nUse File/New to create a new album."))
2851         end
2852     end
2853
2854     if !source = $xmldoc.root.attributes['source']
2855         return utf8(_("Corrupted booh file..."))
2856     end
2857
2858     if !dest = $xmldoc.root.attributes['destination']
2859         return utf8(_("Corrupted booh file..."))
2860     end
2861
2862     if !theme = $xmldoc.root.attributes['theme']
2863         return utf8(_("Corrupted booh file..."))
2864     end
2865
2866     if $xmldoc.root.attributes['version'] < '0.9.0'
2867         msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2868         mark_document_as_dirty
2869         if $xmldoc.root.attributes['version'] < '0.8.4'
2870             msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2871             `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2872                 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2873                 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2874                 if old_dest_dir != new_dest_dir
2875                     sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2876                 end
2877                 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2878                     xmldir.elements.each { |element|
2879                         if %w(image video).include?(element.name) && !element.attributes['deleted']
2880                             old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2881                             new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2882                             Dir[old_name + '*'].each { |file|
2883                                 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2884                                 file != new_file and sys("mv '#{file}' '#{new_file}'")
2885                             }
2886                         end
2887                         if element.name == 'dir' && !element.attributes['deleted']
2888                             old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2889                             new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2890                             old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2891                         end
2892                     }
2893                 else
2894                     msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2895                 end
2896             }
2897         end
2898         $xmldoc.root.add_attribute('version', $VERSION)
2899     end
2900
2901     select_current_theme
2902
2903     $filename = filename
2904     set_mainwindow_title(nil)
2905     $default_size['thumbnails'] =~ /(.*)x(.*)/
2906     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2907     $albums_thumbnail_size =~ /(.*)x(.*)/
2908     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2909
2910     populate_subalbums_treeview(true)
2911
2912     $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
2913     return nil
2914 end
2915
2916 def open_file_user(filename)
2917     result = open_file(filename)
2918     if !result
2919         $config['last-opens'] ||= []
2920         if $config['last-opens'][-1] != utf8(filename)
2921             $config['last-opens'] << utf8(filename)
2922         end
2923         $orig_filename = $filename
2924         $main_window.title = 'booh - ' + File.basename($orig_filename)
2925         tmp = Tempfile.new("boohtemp")
2926         Thread.critical = true
2927         $filename = tmp.path
2928         tmp.close!
2929         #- for security
2930         ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
2931         Thread.critical = false
2932         ios.close
2933         $tempfiles << $filename << "#{$filename}.backup"
2934     else
2935         $orig_filename = nil
2936     end
2937     return result
2938 end
2939
2940 def open_file_popup
2941     if !ask_save_modifications(utf8(_("Save this album?")),
2942                                utf8(_("Do you want to save the changes to this album?")),
2943                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2944         return
2945     end
2946     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2947                                     nil,
2948                                     Gtk::FileChooser::ACTION_OPEN,
2949                                     nil,
2950                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2951     fc.add_shortcut_folder(File.expand_path("~/.booh"))
2952     fc.set_current_folder(File.expand_path("~/.booh"))
2953     fc.transient_for = $main_window
2954     ok = false
2955     while !ok
2956         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2957             push_mousecursor_wait(fc)
2958             msg = open_file_user(fc.filename)
2959             pop_mousecursor(fc)
2960             if msg
2961                 show_popup(fc, msg)
2962                 ok = false
2963             else
2964                 ok = true
2965             end
2966         else
2967             ok = true
2968         end
2969     end
2970     fc.destroy
2971 end
2972
2973 def additional_booh_options
2974     options = ''
2975     if $config['mproc']
2976         options += "--mproc #{$config['mproc'].to_i} "
2977     end
2978     options += "--comments-format '#{$config['comments-format']}' "
2979     if $config['transcode-videos']
2980         options += "--transcode-videos '#{$config['transcode-videos']}' "
2981     end
2982     return options
2983 end
2984
2985 def ask_multi_languages(value)
2986     if ! value.nil?
2987         spl = value.split(',')
2988         value = [ spl[0..-2], spl[-1] ]
2989     end
2990
2991     dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
2992                              $main_window,
2993                              Gtk::Dialog::MODAL,
2994                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2995                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2996
2997     lbl = Gtk::Label.new
2998     lbl.markup = utf8(
2999 _("You can choose to activate <b>multi-languages</b> support for this web-album
3000 (it will work only if you publish your web-album on an Apache web-server). This will
3001 use the MultiViews feature of Apache; the pages will be served according to the
3002 value of the Accept-Language HTTP header sent by the web browsers, so that people
3003 with different languages preferences will be able to browse your web-album with
3004 navigation in their language (if language is available).
3005 "))
3006
3007     dialog.vbox.add(lbl)
3008     dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3009                                                                          add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3010                                                                                                      add(languages = Gtk::Button.new))))
3011
3012     pick_languages = proc {
3013         dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3014                                   $main_window,
3015                                   Gtk::Dialog::MODAL,
3016                                   [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3017                                   [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3018
3019         dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3020         hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3021         cbs = []
3022         SUPPORTED_LANGUAGES.each { |lang|
3023             hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3024             if ! value.nil? && value[0].include?(lang)
3025                 cb.active = true
3026             end
3027             cbs << [ lang, cb ]
3028         }
3029
3030         dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3031         hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3032         fallback_language = nil
3033         hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3034         fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3035         if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3036             fbl_rb.active = true
3037             fallback_language = SUPPORTED_LANGUAGES[0]
3038         end
3039         SUPPORTED_LANGUAGES[1..-1].each { |lang|
3040             hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3041             rb.signal_connect('clicked') { fallback_language = lang }
3042             if ! value.nil? && value[1] == lang
3043                 rb.active = true
3044             end
3045         }
3046
3047         dialog2.window_position = Gtk::Window::POS_MOUSE
3048         dialog2.show_all
3049
3050         resp = nil
3051         dialog2.run { |response|
3052             resp = response
3053             if resp == Gtk::Dialog::RESPONSE_OK
3054                 value = []
3055                 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3056                 value[1] = fallback_language
3057                 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3058             end
3059             dialog2.destroy
3060         }
3061         resp
3062     }
3063
3064     languages.signal_connect('clicked') {
3065         pick_languages.call
3066     }
3067     dialog.window_position = Gtk::Window::POS_MOUSE
3068     if value.nil?
3069         rb_no.active = true
3070     else
3071         rb_yes.active = true
3072         languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3073     end
3074     rb_no.signal_connect('clicked') {
3075         if rb_no.active?
3076             languages.hide
3077         else
3078             if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3079                 rb_no.activate
3080             else
3081                 languages.show
3082             end
3083         end
3084     }
3085     oldval = value
3086     dialog.show_all
3087     if rb_no.active?
3088         languages.hide
3089     end
3090
3091     dialog.run { |response|
3092         if rb_no.active?
3093             value = nil
3094         end
3095         dialog.destroy
3096         if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3097             if value.nil?
3098                 return [ true, nil ]
3099             else
3100                 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3101             end
3102         else
3103             return [ false ]
3104         end
3105     }
3106 end
3107
3108 def new_album
3109     if !ask_save_modifications(utf8(_("Save this album?")),
3110                                utf8(_("Do you want to save the changes to this album?")),
3111                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3112         return
3113     end
3114     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3115                              $main_window,
3116                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3117                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3118                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3119     
3120     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3121     tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3122                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3123     tbl.attach(src = Gtk::Entry.new,
3124                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3125     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3126                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3127     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3128                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3129     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3130                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3131     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3132                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3133     tbl.attach(dest = Gtk::Entry.new,
3134                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3135     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3136                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3137     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3138                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3139     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3140                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3141     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3142                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3143
3144     tooltips = Gtk::Tooltips.new
3145     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3146     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3147                          pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3148     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3149                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3150     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3151     tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of photos from point-and-shoot cameras - also called compact cameras - is 4/3, whereas photos from SLR cameras - also called reflex cameras - is 3/2)")), nil)
3152     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3153                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3154     nperpage_model = Gtk::ListStore.new(String, String)
3155     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3156                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3157     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3158     nperpagecombo.set_attributes(crt, { :markup => 0 })
3159     iter = nperpage_model.append
3160     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3161     iter[1] = nil
3162     [ 12, 20, 30, 40, 50 ].each { |v|
3163         iter = nperpage_model.append
3164         iter[0] = iter[1] = v.to_s
3165     }
3166     nperpagecombo.active = 0
3167
3168     multilanguages_value = nil
3169     vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3170                                         pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3171     tooltips.set_tip(ml, utf8(_("When disabled, the web-album will be generated with navigation in your desktop language. When enabled, the web-album will be generated with navigation in all languages you select, but you have to publish your web-album on an Apache web-server for that feature to work.")), nil)
3172     multilanguages.signal_connect('clicked') {
3173         retval = ask_multi_languages(multilanguages_value)
3174         if retval[0] 
3175             multilanguages_value = retval[1]
3176         end
3177         if multilanguages_value
3178             ml_label.text = utf8(_("Multi-languages: enabled."))
3179         else
3180             ml_label.text = utf8(_("Multi-languages: disabled."))
3181         end
3182     }
3183     if $config['default-multi-languages']
3184         multilanguages_value = $config['default-multi-languages']
3185         ml_label.text = utf8(_("Multi-languages: enabled."))
3186     else
3187         ml_label.text = utf8(_("Multi-languages: disabled."))
3188     end
3189
3190     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3191                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3192     tooltips.set_tip(indexlinkentry, utf8(_("Optional HTML markup to use on pages bottom for a small link returning to wherever you see fit in your website (or somewhere else)")), nil)
3193     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3194                                    pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3195     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)
3196
3197     src_nb_calculated_for = ''
3198     src_nb_thread = nil
3199     process_src_nb = proc {
3200         if src.text != src_nb_calculated_for
3201             src_nb_calculated_for = src.text
3202             if src_nb_thread
3203                 Thread.kill(src_nb_thread)
3204                 src_nb_thread = nil
3205             end
3206             if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3207                 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3208             else
3209                 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3210                     if File.readable?(from_utf8_safe(src_nb_calculated_for))
3211                         src_nb_thread = Thread.new {
3212                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3213                             total = { 'image' => 0, 'video' => 0, nil => 0 }
3214                             `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3215                                 if File.basename(dir) =~ /^\./
3216                                     next
3217                                 else
3218                                     begin
3219                                         Dir.entries(dir.chomp).each { |file|
3220                                             total[entry2type(file)] += 1
3221                                         }
3222                                     rescue Errno::EACCES, Errno::ENOENT
3223                                     end
3224                                 end
3225                             }
3226                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3227                             src_nb_thread = nil
3228                         }
3229                     else
3230                         src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3231                     end
3232                 else
3233                     src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3234                 end
3235             end
3236         end
3237         true
3238     }
3239     timeout_src_nb = Gtk.timeout_add(100) {
3240         process_src_nb.call
3241     }
3242
3243     src_browse.signal_connect('clicked') {
3244         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3245                                         nil,
3246                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
3247                                         nil,
3248                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3249         fc.transient_for = $main_window
3250         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3251             src.text = utf8(fc.filename)
3252             process_src_nb.call
3253             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3254         end
3255         fc.destroy
3256     }
3257
3258     dest_browse.signal_connect('clicked') {
3259         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3260                                         nil,
3261                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
3262                                         nil,
3263                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3264         fc.transient_for = $main_window
3265         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3266             dest.text = utf8(fc.filename)
3267         end
3268         fc.destroy
3269     }
3270
3271     conf_browse.signal_connect('clicked') {
3272         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3273                                         nil,
3274                                         Gtk::FileChooser::ACTION_SAVE,
3275                                         nil,
3276                                         [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3277         fc.transient_for = $main_window
3278         fc.add_shortcut_folder(File.expand_path("~/.booh"))
3279         fc.set_current_folder(File.expand_path("~/.booh"))
3280         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3281             conf.text = utf8(fc.filename)
3282         end
3283         fc.destroy
3284     }
3285
3286     theme_sizes = []
3287     nperrows = []
3288     recreate_theme_config = proc {
3289         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3290         theme_sizes = []
3291         select_theme(theme_button.label, 'all', optimize432.active?, nil)
3292         $images_size.each { |s|
3293             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3294             if !s['optional']
3295                 cb.active = true
3296             end
3297             tooltips.set_tip(cb, utf8(s['description']), nil)
3298             theme_sizes << { :widget => cb, :value => s['name'] }
3299         }
3300         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3301         tooltips = Gtk::Tooltips.new
3302         tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3303         theme_sizes << { :widget => cb, :value => 'original' }
3304         sizes.show_all
3305
3306         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3307         nperrow_group = nil
3308         nperrows = []
3309         $allowed_N_values.each { |n|
3310             if nperrow_group
3311                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3312             else
3313                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3314             end
3315             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3316             if $default_N == n
3317                 rb.active = true
3318             end
3319             nperrows << { :widget => rb, :value => n }
3320         }
3321         nperrowradios.show_all
3322     }
3323     recreate_theme_config.call
3324
3325     theme_button.signal_connect('clicked') {
3326         if newtheme = theme_choose(theme_button.label)
3327             theme_button.label = newtheme
3328             recreate_theme_config.call
3329         end
3330     }
3331
3332     dialog.vbox.add(frame1)
3333     dialog.vbox.add(frame2)
3334     dialog.show_all
3335
3336     keepon = true
3337     ok = true
3338     while keepon
3339         dialog.run { |response|
3340             if response == Gtk::Dialog::RESPONSE_OK
3341                 srcdir = from_utf8_safe(src.text)
3342                 destdir = from_utf8_safe(dest.text)
3343                 confpath = from_utf8_safe(conf.text)
3344                 if src.text != '' && srcdir == ''
3345                     show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3346                     src.grab_focus
3347                 elsif !File.directory?(srcdir)
3348                     show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3349                     src.grab_focus
3350                 elsif dest.text != '' && destdir == ''
3351                     show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3352                     dest.grab_focus
3353                 elsif destdir != make_dest_filename(destdir)
3354                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3355                     dest.grab_focus
3356                 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3357                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3358 inside it will be permanently removed before creating the web-album!
3359 Are you sure you want to continue?")), { :okcancel => true })
3360                     dest.grab_focus
3361                 elsif File.exists?(destdir) && !File.directory?(destdir)
3362                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3363                     dest.grab_focus
3364                 elsif conf.text == ''
3365                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3366                     conf.grab_focus
3367                 elsif conf.text != '' && confpath == ''
3368                     show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3369                     conf.grab_focus
3370                 elsif File.directory?(confpath)
3371                     show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3372                     conf.grab_focus
3373                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3374                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3375                 else
3376                     system("mkdir '#{destdir}'")
3377                     if !File.directory?(destdir)
3378                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3379                         dest.grab_focus
3380                     else
3381                         keepon = false
3382                     end
3383                 end
3384             else
3385                 keepon = ok = false
3386             end
3387         }
3388     end
3389     if ok
3390         srcdir = from_utf8(src.text)
3391         destdir = from_utf8(dest.text)
3392         configskel = File.expand_path(from_utf8(conf.text))
3393         theme = theme_button.label
3394         #- some sort of automatic theme preference
3395         $config['default-theme'] = theme
3396         $config['default-multi-languages'] = multilanguages_value
3397         $config['default-optimize32'] = optimize432.active?.to_s
3398         sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3399         nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3400         nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3401         opt432 = optimize432.active?
3402         madewith = madewithentry.text.gsub('\'', '&#39;')  #- because the parameters to booh-backend are between apostrophes
3403         indexlink = indexlinkentry.text.gsub('\'', '&#39;')
3404     end
3405     if src_nb_thread
3406         Thread.kill(src_nb_thread)
3407         gtk_thread_flush  #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3408     end
3409     dialog.destroy
3410     Gtk.timeout_remove(timeout_src_nb)
3411
3412     if ok
3413         call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3414                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3415                      (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3416                      (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
3417                      "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3418                      utf8(_("Please wait while scanning source directory...")),
3419                      'full scan',
3420                      { :closure_after => proc {
3421                              open_file_user(configskel)
3422                              $main_window.urgency_hint = true
3423                          } })
3424     end
3425 end
3426
3427 def properties
3428     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3429                              $main_window,
3430                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3431                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3432                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3433     
3434     source = $xmldoc.root.attributes['source']
3435     dest = $xmldoc.root.attributes['destination']
3436     theme = $xmldoc.root.attributes['theme']
3437     opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3438     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3439     nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3440     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3441     if limit_sizes
3442         limit_sizes = limit_sizes.split(/,/)
3443     end
3444     madewith = ($xmldoc.root.attributes['made-with'] || '').gsub('&#39;', '\'')
3445     indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub('&#39;', '\'')
3446     save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3447
3448     tooltips = Gtk::Tooltips.new
3449     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3450     tbl.attach(Gtk::Label.new(utf8(_("Directory of source photos/videos: "))),
3451                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3452     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3453                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3454     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3455                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3456     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3457                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3458     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3459                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3460     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3461                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3462
3463     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3464     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3465                                    pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3466     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3467                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3468     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3469     tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of photos from point-and-shoot cameras - also called compact cameras - is 4/3, whereas photos from SLR cameras - also called reflex cameras - is 3/2)")), nil)
3470     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3471                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3472     nperpage_model = Gtk::ListStore.new(String, String)
3473     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3474                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3475     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3476     nperpagecombo.set_attributes(crt, { :markup => 0 })
3477     iter = nperpage_model.append
3478     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3479     iter[1] = nil
3480     [ 12, 20, 30, 40, 50 ].each { |v|
3481         iter = nperpage_model.append
3482         iter[0] = iter[1] = v.to_s
3483         if nperpage && nperpage == v.to_s
3484             nperpagecombo.active_iter = iter
3485         end
3486     }
3487     if nperpagecombo.active_iter.nil?
3488         nperpagecombo.active = 0
3489     end
3490
3491     vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3492                                         pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3493     tooltips.set_tip(ml, utf8(_("When disabled, the web-album will be generated with navigation in your desktop language. When enabled, the web-album will be generated with navigation in all languages you select, but you have to publish your web-album on an Apache web-server for that feature to work.")), nil)
3494     ml_update = proc {
3495         if save_multilanguages_value
3496             ml_label.text = utf8(_("Multi-languages: enabled."))
3497         else
3498             ml_label.text = utf8(_("Multi-languages: disabled."))
3499         end
3500     }
3501     ml_update.call
3502     multilanguages.signal_connect('clicked') {
3503         retval = ask_multi_languages(save_multilanguages_value)
3504         if retval[0] 
3505             save_multilanguages_value = retval[1]
3506         end
3507         ml_update.call
3508     }
3509
3510     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3511                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3512     if indexlink
3513         indexlinkentry.text = indexlink
3514     end
3515     tooltips.set_tip(indexlinkentry, utf8(_("Optional HTML markup to use on pages bottom for a small link returning to wherever you see fit in your website (or somewhere else)")), nil)
3516     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3517                                    pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3518     if madewith
3519         madewithentry.text = madewith
3520     end
3521     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)
3522
3523     theme_sizes = []
3524     nperrows = []
3525     recreate_theme_config = proc {
3526         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3527         theme_sizes = []
3528         select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3529
3530         $images_size.each { |s|
3531             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3532             if limit_sizes
3533                 if limit_sizes.include?(s['name'])
3534                     cb.active = true
3535                 end
3536             else
3537                 if !s['optional']
3538                     cb.active = true
3539                 end
3540             end
3541             tooltips.set_tip(cb, utf8(s['description']), nil)
3542             theme_sizes << { :widget => cb, :value => s['name'] }
3543         }
3544         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3545         tooltips = Gtk::Tooltips.new
3546         tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3547         if limit_sizes && limit_sizes.include?('original')
3548             cb.active = true
3549         end
3550         theme_sizes << { :widget => cb, :value => 'original' }
3551         sizes.show_all
3552
3553         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3554         nperrow_group = nil
3555         nperrows = []
3556         $allowed_N_values.each { |n|
3557             if nperrow_group
3558                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3559             else
3560                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3561             end
3562             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3563             nperrowradios.add(Gtk::Label.new('  '))
3564             if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3565                 rb.active = true
3566             end
3567             nperrows << { :widget => rb, :value => n.to_s }
3568         }
3569         nperrowradios.show_all
3570     }
3571     recreate_theme_config.call
3572
3573     theme_button.signal_connect('clicked') {
3574         if newtheme = theme_choose(theme_button.label)
3575             limit_sizes = nil
3576             nperrow = nil
3577             theme_button.label = newtheme
3578             recreate_theme_config.call
3579         end
3580     }
3581
3582     dialog.vbox.add(frame1)
3583     dialog.vbox.add(frame2)
3584     dialog.show_all
3585
3586     keepon = true
3587     ok = true
3588     while keepon
3589         dialog.run { |response|
3590             if response == Gtk::Dialog::RESPONSE_OK
3591                 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3592                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3593                 else
3594                     keepon = false
3595                 end
3596             else
3597                 keepon = ok = false
3598             end
3599         }
3600     end
3601     save_theme = theme_button.label
3602     save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3603     save_opt432 = optimize432.active?
3604     save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3605     save_nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3606     save_madewith = madewithentry.text.gsub('\'', '&#39;')  #- because the parameters to booh-backend are between apostrophes
3607     save_indexlink = indexlinkentry.text.gsub('\'', '&#39;')
3608     dialog.destroy
3609     
3610     if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_nperpage != nperpage || save_madewith != madewith || save_indexlink != indexlinkentry || save_multilanguages_value != multilanguages_value)
3611         #- some sort of automatic preferences
3612         if save_theme != theme
3613             $config['default-theme'] = save_theme
3614         end
3615         if save_multilanguages_value != multilanguages_value
3616             $config['default-multi-languages'] = save_multilanguages_value
3617         end
3618         if save_opt432 != opt432
3619             $config['default-optimize32'] = save_opt432.to_s
3620         end
3621         mark_document_as_dirty
3622         save_current_file
3623         call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3624                      "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3625                      (save_nperpage ? "--thumbnails-per-page #{save_nperpage} " : '') +
3626                      (save_multilanguages_value ? "--multi-languages #{save_multilanguages_value} " : '') +
3627                      "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3628                      utf8(_("Please wait while scanning source directory...")),
3629                      'full scan',
3630                      { :closure_after => proc {
3631                              open_file($filename)
3632                              $modified = true
3633                              $main_window.urgency_hint = true
3634                          } })
3635     else
3636         #- select_theme merges global variables, need to return to current choices
3637         select_current_theme
3638     end
3639 end
3640
3641 def merge_current
3642     save_current_file
3643
3644     sel = $albums_tv.selection.selected_rows
3645
3646     call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3647                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
3648                  utf8(_("Please wait while scanning source directory...")),
3649                  'one dir scan',
3650                  { :closure_after => proc {
3651                          open_file($filename)
3652                          $albums_tv.selection.select_path(sel[0])
3653                          $modified = true
3654                          $main_window.urgency_hint = true
3655                      } })
3656 end
3657
3658 def merge_newsubs
3659     save_current_file
3660
3661     sel = $albums_tv.selection.selected_rows
3662
3663     call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3664                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
3665                  utf8(_("Please wait while scanning source directory...")),
3666                  'subdirs scan',
3667                  { :closure_after => proc {
3668                          open_file($filename)
3669                          $albums_tv.selection.select_path(sel[0])
3670                          $modified = true
3671                          $main_window.urgency_hint = true
3672                      } })
3673 end
3674
3675 def merge
3676     save_current_file
3677
3678     theme = $xmldoc.root.attributes['theme']
3679     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3680     if limit_sizes
3681         limit_sizes = "--sizes #{limit_sizes}"
3682     end
3683     call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3684                  "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3685                  utf8(_("Please wait while scanning source directory...")),
3686                  'full scan',
3687                  { :closure_after => proc {
3688                          open_file($filename)
3689                          $modified = true
3690                          $main_window.urgency_hint = true
3691                      } })
3692 end
3693
3694 def save_as_do
3695     fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3696                                     nil,
3697                                     Gtk::FileChooser::ACTION_SAVE,
3698                                     nil,
3699                                     [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3700     fc.transient_for = $main_window
3701     fc.add_shortcut_folder(File.expand_path("~/.booh"))
3702     fc.set_current_folder(File.expand_path("~/.booh"))
3703     fc.filename = $orig_filename
3704     if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3705         $orig_filename = fc.filename
3706         if ! save_current_file_user
3707             fc.destroy
3708             return save_as_do
3709         end
3710         $config['last-opens'] ||= []
3711         $config['last-opens'] << $orig_filename
3712     end
3713     fc.destroy
3714 end
3715
3716 def preferences
3717     dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3718                              $main_window,
3719                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3720                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3721                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3722
3723     dialog.vbox.add(notebook = Gtk::Notebook.new)
3724     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3725     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3726                0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3727     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer']).set_size_request(250, -1)),
3728                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3729     tooltips = Gtk::Tooltips.new
3730     tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3731 for example: /usr/bin/mplayer %f")), nil)
3732     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3733                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3734     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3735                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3736     tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3737 for example: /usr/bin/gimp-remote %f")), nil)
3738     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3739                0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3740     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3741                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3742     tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3743 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3744     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3745                0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3746     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)),
3747                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3748     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)
3749     tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3750                0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3751     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)
3752     tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original photos/videos as well"))),
3753                0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3754     tooltips.set_tip(deleteondisk_check, utf8(_("Normally, deleting a photo 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)
3755
3756     smp_check.signal_connect('toggled') {
3757         smp_hbox.sensitive = smp_check.active?
3758     }
3759     if $config['mproc']
3760         smp_check.active = true
3761         smp_spin.value = $config['mproc'].to_i
3762     end
3763     nogestures_check.active = $config['nogestures']
3764     deleteondisk_check.active = $config['deleteondisk']
3765
3766     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3767     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3768                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3769     tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3770                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3771     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nphotos in new albums:"))),
3772                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3773     tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3774                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3775     tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3776                2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3777     tooltips.set_tip(commentsformat_entry, utf8(_("Normally, filenames without extension are used as comments for photos and videos in new albums. Use this entry to use something else.")), nil)
3778     commentsformat_help.signal_connect('clicked') {
3779         show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3780 hence you should look at ImageMagick/identify documentation for the most    
3781 accurate and up-to-date documentation. Last time I checked, documentation
3782 was:
3783
3784 Print information about the image in a format of your choosing. You can
3785 include the image filename, type, width, height, Exif data, or other image
3786 attributes by embedding special format characters:                          
3787
3788                      %O   page offset
3789                      %P   page width and height                             
3790                      %b   file size                                         
3791                      %c   comment                                           
3792                      %d   directory                                         
3793                      %e   filename extension                                
3794                      %f   filename                                          
3795                      %g   page geometry                                     
3796                      %h   height                                            
3797                      %i   input filename                                    
3798                      %k   number of unique colors                           
3799                      %l   label                                             
3800                      %m   magick                                            
3801                      %n   number of scenes                                  
3802                      %o   output filename                                   
3803                      %p   page number                                       
3804                      %q   quantum depth                                     
3805                      %r   image class and colorspace                        
3806                      %s   scene number                                      
3807                      %t   top of filename                                   
3808                      %u   unique temporary filename                         
3809                      %w   width                                             
3810                      %x   x resolution                                      
3811                      %y   y resolution                                      
3812                      %z   image depth                                       
3813                      %@   bounding box                                      
3814                      %#   signature                                         
3815                      %%   a percent sign                                    
3816                                                                             
3817 For example,                                                                
3818                                                                             
3819     %m:%f %wx%h
3820                                                                             
3821 displays MIFF:bird.miff 512x480 for an image titled bird.miff and whose
3822 width is 512 and height is 480.                
3823                                                                             
3824 If the first character of string is @, the format is read from a file titled
3825 by the remaining characters in the string.
3826                                                                             
3827 You can also use the following special formatting syntax to print Exif
3828 information contained in the file:
3829                                                                             
3830     %[EXIF:tag]                                                             
3831                                                                             
3832 Where tag can be one of the following:                                      
3833                                                                             
3834     *  (print all Exif tags, in keyword=data format)                        
3835     !  (print all Exif tags, in tag_number data format)                     
3836     #hhhh (print data for Exif tag #hhhh)                                   
3837     ImageWidth                                                              
3838     ImageLength                                                             
3839     BitsPerSample                                                           
3840     Compression                                                             
3841     PhotometricInterpretation                                               
3842     FillOrder                                                               
3843     DocumentName                                                            
3844     ImageDescription                                                        
3845     Make                                                                    
3846     Model                                                                   
3847     StripOffsets                                                            
3848     Orientation                                                             
3849     SamplesPerPixel                                                         
3850     RowsPerStrip                                                            
3851     StripByteCounts                                                         
3852     XResolution                                                             
3853     YResolution                                                             
3854     PlanarConfiguration                                                     
3855     ResolutionUnit                                                          
3856     TransferFunction                                                        
3857     Software                                                                
3858     DateTime                                                                
3859     Artist                                                                  
3860     WhitePoint                                                              
3861     PrimaryChromaticities                                                   
3862     TransferRange                                                           
3863     JPEGProc                                                                
3864     JPEGInterchangeFormat                                                   
3865     JPEGInterchangeFormatLength                                             
3866     YCbCrCoefficients                                                       
3867     YCbCrSubSampling                                                        
3868     YCbCrPositioning                                                        
3869     ReferenceBlackWhite                                                     
3870     CFARepeatPatternDim                                                     
3871     CFAPattern                                                              
3872     BatteryLevel                                                            
3873     Copyright                                                               
3874     ExposureTime                                                            
3875     FNumber                                                                 
3876     IPTC/NAA                                                                
3877     ExifOffset                                                              
3878     InterColorProfile                                                       
3879     ExposureProgram                                                         
3880     SpectralSensitivity                                                     
3881     GPSInfo                                                                 
3882     ISOSpeedRatings                                                         
3883     OECF                                                                    
3884     ExifVersion                                                             
3885     DateTimeOriginal                                                        
3886     DateTimeDigitized                                                       
3887     ComponentsConfiguration                                                 
3888     CompressedBitsPerPixel                                                  
3889     ShutterSpeedValue                                                       
3890     ApertureValue                                                           
3891     BrightnessValue                                                         
3892     ExposureBiasValue                                                       
3893     MaxApertureValue                                                        
3894     SubjectDistance                                                         
3895     MeteringMode                                                            
3896     LightSource                                                             
3897     Flash                                                                   
3898     FocalLength                                                             
3899     MakerNote                                                               
3900     UserComment                                                             
3901     SubSecTime                                                              
3902     SubSecTimeOriginal                                                      
3903     SubSecTimeDigitized                                                     
3904     FlashPixVersion                                                         
3905     ColorSpace                                                              
3906     ExifImageWidth                                                          
3907     ExifImageLength                                                         
3908     InteroperabilityOffset                                                  
3909     FlashEnergy                                                             
3910     SpatialFrequencyResponse                                                
3911     FocalPlaneXResolution                                                   
3912     FocalPlaneYResolution                                                   
3913     FocalPlaneResolutionUnit                                                
3914     SubjectLocation                                                         
3915     ExposureIndex                                                           
3916     SensingMethod                                                           
3917     FileSource                                                              
3918     SceneType")), { :scrolled => true })
3919     }
3920     tbl.attach(update_exif_orientation_check = Gtk::CheckButton.new(utf8(_("Update file's EXIF orientation when rotating a picture"))),
3921                0, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3922     tooltips.set_tip(update_exif_orientation_check, utf8(_("When rotating a picture (Alt-Right/Left), also update EXIF orientation in the file itself")), nil)
3923     update_exif_orientation_check.active = $config['rotate-set-exif'] == 'true'
3924     tbl.attach(transcode_videos = Gtk::CheckButton.new(utf8(_("Transcode videos"))).set_active(!$config['transcode-videos'].nil?),
3925                0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3926     transcode_videos.active = ! $config['transcode-videos'].nil?
3927     tbl.attach(transcode_videos_command = Gtk::Entry.new.set_text($config['transcode-videos'] || 'avi:mencoder -nosound -ovc xvid -xvidencopts bitrate=800:me_quality=6 -o %o %f'),
3928                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3929     tooltips.set_tip(transcode_videos, utf8(_("Whether to transcode videos into the web-album instead of using the original videos directly (can be an interesting disk space saver!). First put the extension of the output video and a colon; then use %f to specify the input and %o the output;
3930 for example: avi:mencoder -nosound -ovc xvid -xvidencopts bitrate=800:me_quality=6 -o %o %f")), nil)
3931     transcode_videos.signal_connect('toggled') {
3932         transcode_videos_command.sensitive = transcode_videos.active?
3933     }
3934     transcode_videos_command.sensitive = transcode_videos.active?
3935
3936     dialog.vbox.show_all
3937     dialog.run { |response|
3938         if response == Gtk::Dialog::RESPONSE_OK
3939             $config['video-viewer'] = from_utf8(video_viewer_entry.text)
3940             $config['image-editor'] = from_utf8(image_editor_entry.text)
3941             $config['browser'] = from_utf8(browser_entry.text)
3942             if smp_check.active?
3943                 $config['mproc'] = smp_spin.value.to_i
3944             else
3945                 $config.delete('mproc')
3946             end
3947             $config['nogestures'] = nogestures_check.active?
3948             $config['deleteondisk'] = deleteondisk_check.active?
3949
3950             $config['convert-enhance'] = from_utf8(enhance_entry.text)
3951             $config['comments-format'] = from_utf8(commentsformat_entry.text.gsub(/'/, ''))
3952             $config['rotate-set-exif'] = update_exif_orientation_check.active?.to_s
3953             if transcode_videos.active?
3954                 $config['transcode-videos'] = transcode_videos_command.text
3955             else
3956                 $config.delete('transcode-videos')
3957             end
3958         end
3959     }
3960     dialog.destroy
3961 end
3962
3963 def perform_undo
3964     if $undo_tb.sensitive?
3965         $redo_tb.sensitive = $redo_mb.sensitive = true
3966         if not more_undoes = UndoHandler.undo($statusbar)
3967             $undo_tb.sensitive = $undo_mb.sensitive = false
3968         end
3969     end
3970 end
3971
3972 def perform_redo
3973     if $redo_tb.sensitive?
3974         $undo_tb.sensitive = $undo_mb.sensitive = true
3975         if not more_redoes = UndoHandler.redo($statusbar)
3976             $redo_tb.sensitive = $redo_mb.sensitive =&nb