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