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