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