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