multiviews booh.org
[booh] / bin / booh
1 #! /usr/bin/ruby
2 #
3 #                         *  BOOH  *
4 #
5 # A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
6 #
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
11 #
12 #
13 # Copyright (c) 2004-2008 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
14 #
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21
22 require 'getoptlong'
23 require 'tempfile'
24 require 'thread'
25
26 require 'gtk2'
27 require 'booh/libadds'
28 require 'booh/GtkAutoTable'
29
30 require 'gettext'
31 include GetText
32 bindtextdomain("booh")
33
34 require 'booh/rexml/document'
35 include REXML
36
37 require 'booh/booh-lib'
38 include Booh
39 require 'booh/UndoHandler'
40 require 'booh/Synchronizator'
41
42
43 #- options
44 $options = [
45     [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       _("Get help message") ],
46
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             #- cannot use sizeobj because panoramic images will have a larger width
1143             Dir.glob("#{dest_img_base}-*.jpg") do |file|
1144                 File.delete(file)
1145             end
1146         end
1147
1148     }
1149
1150     refresh = proc {
1151         cleanup_all_thumbnails.call
1152         #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1153         $modified = true
1154         $xmldir.delete_attribute('already-generated')
1155         my_gen_real_thumbnail.call
1156     }
1157  
1158     rotate_and_cleanup = proc { |angle|
1159         cleanup_all_thumbnails.call
1160         rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1161     }
1162
1163     move = proc { |direction|
1164         do_method = "move_#{direction}"
1165         undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1166         perform = proc {
1167             done = autotable.method(do_method).call(vbox)
1168             textview.grab_focus  #- because if moving, focus is stolen
1169             done
1170         }
1171         if perform.call
1172             save_undo(_("move %s") % direction,
1173                       proc {
1174                           autotable.method(undo_method).call(vbox)
1175                           textview.grab_focus  #- because if moving, focus is stolen
1176                           autoscroll_if_needed($autotable_sw, img, textview)
1177                           $notebook.set_page(1)
1178                           proc {
1179                               autotable.method(do_method).call(vbox)
1180                               textview.grab_focus  #- because if moving, focus is stolen
1181                               autoscroll_if_needed($autotable_sw, img, textview)
1182                               $notebook.set_page(1)
1183                           }
1184                       })
1185         end
1186     }
1187
1188     color_swap_and_cleanup = proc {
1189         perform_color_swap_and_cleanup = proc {
1190             cleanup_all_thumbnails.call
1191             color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1192             my_gen_real_thumbnail.call
1193         }
1194
1195         perform_color_swap_and_cleanup.call
1196
1197         save_undo(_("color swap"),
1198                   proc {
1199                       perform_color_swap_and_cleanup.call
1200                       textview.grab_focus
1201                       autoscroll_if_needed($autotable_sw, img, textview)
1202                       $notebook.set_page(1)
1203                       proc {
1204                           perform_color_swap_and_cleanup.call
1205                           textview.grab_focus
1206                           autoscroll_if_needed($autotable_sw, img, textview)
1207                           $notebook.set_page(1)
1208                       }
1209                   })
1210     }
1211
1212     change_seektime_and_cleanup_real = proc { |values|
1213         perform_change_seektime_and_cleanup = proc { |val|
1214             cleanup_all_thumbnails.call
1215             change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1216             my_gen_real_thumbnail.call
1217         }
1218         perform_change_seektime_and_cleanup.call(values[:new])
1219         
1220         save_undo(_("specify seektime"),
1221                   proc {
1222                       perform_change_seektime_and_cleanup.call(values[:old])
1223                       textview.grab_focus
1224                       autoscroll_if_needed($autotable_sw, img, textview)
1225                       $notebook.set_page(1)
1226                       proc {
1227                           perform_change_seektime_and_cleanup.call(values[:new])
1228                           textview.grab_focus
1229                           autoscroll_if_needed($autotable_sw, img, textview)
1230                           $notebook.set_page(1)
1231                       }
1232                   })
1233     }
1234
1235     change_seektime_and_cleanup = proc {
1236         if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1237             change_seektime_and_cleanup_real.call(values)
1238         end
1239     }
1240
1241     change_pano_amount_and_cleanup_real = proc { |values|
1242         perform_change_pano_amount_and_cleanup = proc { |val|
1243             cleanup_all_thumbnails.call
1244             change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1245         }
1246         perform_change_pano_amount_and_cleanup.call(values[:new])
1247         
1248         save_undo(_("change panorama amount"),
1249                   proc {
1250                       perform_change_pano_amount_and_cleanup.call(values[:old])
1251                       textview.grab_focus
1252                       autoscroll_if_needed($autotable_sw, img, textview)
1253                       $notebook.set_page(1)
1254                       proc {
1255                           perform_change_pano_amount_and_cleanup.call(values[:new])
1256                           textview.grab_focus
1257                           autoscroll_if_needed($autotable_sw, img, textview)
1258                           $notebook.set_page(1)
1259                       }
1260                   })
1261     }
1262
1263     change_pano_amount_and_cleanup = proc {
1264         if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1265             change_pano_amount_and_cleanup_real.call(values)
1266         end
1267     }
1268
1269     whitebalance_and_cleanup_real = proc { |values|
1270         perform_change_whitebalance_and_cleanup = proc { |val|
1271             cleanup_all_thumbnails.call
1272             change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1273             recalc_whitebalance(val, fullpath, thumbnail_img, img,
1274                                 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1275         }
1276         perform_change_whitebalance_and_cleanup.call(values[:new])
1277
1278         save_undo(_("fix white balance"),
1279                   proc {
1280                       perform_change_whitebalance_and_cleanup.call(values[:old])
1281                       textview.grab_focus
1282                       autoscroll_if_needed($autotable_sw, img, textview)
1283                       $notebook.set_page(1)
1284                       proc {
1285                           perform_change_whitebalance_and_cleanup.call(values[:new])
1286                           textview.grab_focus
1287                           autoscroll_if_needed($autotable_sw, img, textview)
1288                           $notebook.set_page(1)
1289                       }
1290                   })
1291     }
1292
1293     whitebalance_and_cleanup = proc {
1294         if values = ask_whitebalance(fullpath, thumbnail_img, img,
1295                                      $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1296             whitebalance_and_cleanup_real.call(values)
1297         end
1298     }
1299
1300     gammacorrect_and_cleanup_real = proc { |values|
1301         perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1302             cleanup_all_thumbnails.call
1303             change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1304             recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1305                                 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1306         }
1307         perform_change_gammacorrect_and_cleanup.call(values[:new])
1308         
1309         save_undo(_("gamma correction"),
1310                   Proc.new {
1311                       perform_change_gammacorrect_and_cleanup.call(values[:old])
1312                       textview.grab_focus
1313                       autoscroll_if_needed($autotable_sw, img, textview)
1314                       $notebook.set_page(1)
1315                       Proc.new {
1316                           perform_change_gammacorrect_and_cleanup.call(values[:new])
1317                           textview.grab_focus
1318                           autoscroll_if_needed($autotable_sw, img, textview)
1319                           $notebook.set_page(1)
1320                       }
1321                   })
1322     }
1323     
1324     gammacorrect_and_cleanup = Proc.new {
1325         if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1326                                      $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1327             gammacorrect_and_cleanup_real.call(values)
1328         end
1329     }
1330     
1331     enhance_and_cleanup = proc {
1332         perform_enhance_and_cleanup = proc {
1333             cleanup_all_thumbnails.call
1334             enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1335             my_gen_real_thumbnail.call
1336         }
1337         
1338         cleanup_all_thumbnails.call
1339         perform_enhance_and_cleanup.call
1340
1341         save_undo(_("enhance"),
1342                   proc {
1343                       perform_enhance_and_cleanup.call
1344                       textview.grab_focus
1345                       autoscroll_if_needed($autotable_sw, img, textview)
1346                       $notebook.set_page(1)
1347                       proc {
1348                           perform_enhance_and_cleanup.call
1349                           textview.grab_focus
1350                           autoscroll_if_needed($autotable_sw, img, textview)
1351                           $notebook.set_page(1)
1352                       }
1353                   })
1354     }
1355
1356     delete = proc { |isacut|
1357         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 })
1358             $modified = true
1359             after = nil
1360             perform_delete = proc {
1361                 after = autotable.get_next_widget(vbox)
1362                 if !after
1363                     after = autotable.get_previous_widget(vbox)
1364                 end
1365                 if $config['deleteondisk'] && !isacut
1366                     msg 3, "scheduling for delete: #{fullpath}"
1367                     $todelete << fullpath
1368                 end
1369                 autotable.remove_widget(vbox)
1370                 if after
1371                     $vbox2widgets[after][:textview].grab_focus
1372                     autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1373                 end
1374             }
1375             
1376             previous_pos = autotable.get_current_number(vbox)
1377             perform_delete.call
1378
1379             if !after
1380                 delete_current_subalbum
1381             else
1382                 save_undo(_("delete"),
1383                           proc { |pos|
1384                               autotable.reinsert(pos, vbox, filename)
1385                               $notebook.set_page(1)
1386                               autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1387                               $cuts = []
1388                               msg 3, "removing deletion schedule of: #{fullpath}"
1389                               $todelete.delete(fullpath)  #- unconditional because deleteondisk option could have been modified
1390                               proc {
1391                                   perform_delete.call
1392                                   $notebook.set_page(1)
1393                               }
1394                           }, previous_pos)
1395             end
1396         end
1397     }
1398
1399     cut = proc {
1400         delete.call(true)
1401         $cuts << { :vbox => vbox, :filename => filename }
1402         $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1403     }
1404     paste = proc {
1405         if $cuts.size > 0
1406             $cuts.each { |elem|
1407                 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1408             }
1409             last = $cuts[-1]
1410             autotable.queue_draws << proc {
1411                 $vbox2widgets[last[:vbox]][:textview].grab_focus
1412                 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1413             }
1414             save_undo(_("paste"),
1415                       proc { |cuts|
1416                           cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1417                           $notebook.set_page(1)
1418                           proc {
1419                               cuts.each { |elem|
1420                                   autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1421                               }
1422                               $notebook.set_page(1)
1423                           }
1424                       }, $cuts)
1425             $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1426             $cuts = []
1427         end
1428     }
1429
1430     $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1431                                  :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1432                                  :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1433                                  :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1434
1435     textview.signal_connect('key-press-event') { |w, event|
1436         propagate = true
1437         if event.state != 0
1438             x, y = autotable.get_current_pos(vbox)
1439             control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1440             shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1441             alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1442             if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1443                 if control_pressed
1444                     if widget_up = autotable.get_widget_at_pos(x, y - 1)
1445                         $vbox2widgets[widget_up][:textview].grab_focus
1446                     end
1447                 end
1448                 if shift_pressed
1449                     move.call('up')
1450                 end
1451             end
1452             if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1453                 if control_pressed
1454                     if widget_down = autotable.get_widget_at_pos(x, y + 1)
1455                         $vbox2widgets[widget_down][:textview].grab_focus
1456                     end
1457                 end
1458                 if shift_pressed
1459                     move.call('down')
1460                 end
1461             end
1462             if event.keyval == Gdk::Keyval::GDK_Left
1463                 if x > 0
1464                     if control_pressed
1465                         $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1466                     end
1467                     if shift_pressed
1468                         move.call('left')
1469                     end
1470                 end
1471                 if alt_pressed
1472                     rotate_and_cleanup.call(-90)
1473                 end
1474             end
1475             if event.keyval == Gdk::Keyval::GDK_Right
1476                 next_ = autotable.get_next_widget(vbox)
1477                 if next_ && autotable.get_current_pos(next_)[0] > x
1478                     if control_pressed
1479                         $vbox2widgets[next_][:textview].grab_focus
1480                     end
1481                     if shift_pressed
1482                         move.call('right')
1483                     end
1484                 end
1485                 if alt_pressed
1486                     rotate_and_cleanup.call(90)
1487                 end
1488             end
1489             if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1490                 delete.call(false)
1491             end
1492             if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1493                 view_element(filename, { :delete => delete })
1494                 propagate = false
1495             end
1496             if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1497                 perform_undo
1498             end
1499             if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1500                 perform_redo
1501             end
1502         end
1503         !propagate  #- propagate if needed
1504     }
1505
1506     $ignore_next_release = false
1507     evtbox.signal_connect('button-press-event') { |w, event|
1508         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1509             if event.state & Gdk::Window::BUTTON3_MASK != 0
1510                 #- gesture redo: hold right mouse button then click left mouse button
1511                 $config['nogestures'] or perform_redo
1512                 $ignore_next_release = true
1513             else
1514                 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1515                 if $r90.active?
1516                     rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1517                 elsif $r270.active?
1518                     rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1519                 elsif $enhance.active?
1520                     enhance_and_cleanup.call
1521                 elsif $delete.active?
1522                     delete.call(false)
1523                 else
1524                     textview.grab_focus
1525                     $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1526                 end
1527             end
1528             $button1_pressed_autotable = true
1529         elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1530             if event.state & Gdk::Window::BUTTON1_MASK != 0
1531                 #- gesture undo: hold left mouse button then click right mouse button
1532                 $config['nogestures'] or perform_undo
1533                 $ignore_next_release = true
1534             end
1535         elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1536             view_element(filename, { :delete => delete })
1537         end
1538         false   #- propagate
1539     }
1540
1541     evtbox.signal_connect('button-release-event') { |w, event|
1542         if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1543             if !$ignore_next_release
1544                 x, y = autotable.get_current_pos(vbox)
1545                 next_ = autotable.get_next_widget(vbox)
1546                 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1547                                      { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1548                                        :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1549                                      { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1550                                        :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1551                                        :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1552                                        :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1553             end
1554             $ignore_next_release = false
1555             $gesture_press = nil
1556         end
1557         false   #- propagate
1558     }
1559
1560     #- handle reordering with drag and drop
1561     Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1562     Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1563     vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1564         selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1565     }
1566
1567     vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1568         done = false
1569         #- mouse gesture first (dnd disables button-release-event)
1570         if $gesture_press && $gesture_press[:filename] == filename
1571             if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1572                 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1573                 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1574                 rotate_and_cleanup.call(angle)
1575                 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1576                 done = true
1577             elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1578                 msg 3, "gesture delete: click-drag right button to the bottom"
1579                 delete.call(false)
1580                 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1581                 done = true
1582             end
1583         end
1584         if !done
1585             ctxt.targets.each { |target|
1586                 if target.name == 'reorder-elements'
1587                     move_dnd = proc { |from,to|
1588                         if from != to
1589                             $modified = true
1590                             autotable.move(from, to)
1591                             save_undo(_("reorder"),
1592                                       proc { |from, to|
1593                                           if to > from
1594                                               autotable.move(to - 1, from)
1595                                           else
1596                                               autotable.move(to, from + 1)
1597                                           end
1598                                           $notebook.set_page(1)
1599                                           proc {
1600                                               autotable.move(from, to)
1601                                               $notebook.set_page(1)
1602                                           }
1603                                       }, from, to)
1604                         end
1605                     }
1606                     if $multiple_dnd.size == 0
1607                         move_dnd.call(selection_data.data.to_i,
1608                                       autotable.get_current_number(vbox))
1609                     else
1610                         UndoHandler.begin_batch
1611                         $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1612                                       each { |path|
1613                             #- need to update current position between each call
1614                             move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1615                                           autotable.get_current_number(vbox))
1616                         }
1617                         UndoHandler.end_batch
1618                     end
1619                     $multiple_dnd = []
1620                 end
1621             }
1622         end
1623     }
1624
1625     vbox.show_all
1626 end
1627
1628 def create_auto_table
1629
1630     $autotable = Gtk::AutoTable.new(5)
1631
1632     $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1633     thumbnails_vb = Gtk::VBox.new(false, 5)
1634
1635     frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1636     $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1637     thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1638     thumbnails_vb.add($autotable)
1639
1640     $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1641     $autotable_sw.add_with_viewport(thumbnails_vb)
1642
1643     #- follows stuff for handling multiple elements selection
1644     press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1645     gc = nil
1646     update_selected = proc {
1647         $autotable.current_order.each { |path|
1648             w = $name2widgets[path][:evtbox].window
1649             xm = w.position[0] + w.size[0]/2
1650             ym = w.position[1] + w.size[1]/2
1651             if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1652                 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1653                     $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1654                     $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1655                 end
1656             end
1657             if $selected_elements[path] && ! $selected_elements[path][:keep]
1658                 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))
1659                     $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1660                     $selected_elements.delete(path)
1661                 end
1662             end
1663         }
1664     }
1665     $autotable.signal_connect('realize') { |w,e|
1666         gc = Gdk::GC.new($autotable.window)
1667         gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1668         gc.function = Gdk::GC::INVERT
1669         #- autoscroll handling for DND and multiple selections
1670         Gtk.timeout_add(100) {
1671             if ! $autotable.window.nil?
1672                 w, x, y, mask = $autotable.window.pointer
1673                 if mask & Gdk::Window::BUTTON1_MASK != 0
1674                     if y < $autotable_sw.vadjustment.value
1675                         if pos_x
1676                             $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1677                         end
1678                         if $button1_pressed_autotable || press_x
1679                             scroll_upper($autotable_sw, y)
1680                         end
1681                         if not press_x.nil?
1682                             w, pos_x, pos_y = $autotable.window.pointer
1683                             $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]])
1684                             update_selected.call
1685                         end
1686                     end
1687                     if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1688                         if pos_x
1689                             $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1690                         end
1691                         if $button1_pressed_autotable || press_x
1692                             scroll_lower($autotable_sw, y)
1693                         end
1694                         if not press_x.nil?
1695                             w, pos_x, pos_y = $autotable.window.pointer
1696                             $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]])
1697                             update_selected.call
1698                         end
1699                     end
1700                 end
1701             end
1702             ! $autotable.window.nil?
1703         }
1704     }
1705
1706     $autotable.signal_connect('button-press-event') { |w,e|
1707         if e.button == 1
1708             if !$button1_pressed_autotable
1709                 press_x = e.x
1710                 press_y = e.y
1711                 if e.state & Gdk::Window::SHIFT_MASK == 0
1712                     $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1713                     $selected_elements = {}
1714                     $statusbar.push(0, utf8(_("Nothing selected.")))
1715                 else
1716                     $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1717                 end
1718                 set_mousecursor(Gdk::Cursor::TCROSS)
1719             end
1720         end
1721     }
1722     $autotable.signal_connect('button-release-event') { |w,e|
1723         if e.button == 1
1724             if $button1_pressed_autotable
1725                 #- unselect all only now
1726                 $multiple_dnd = $selected_elements.keys
1727                 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1728                 $selected_elements = {}
1729                 $button1_pressed_autotable = false
1730             else
1731                 if pos_x
1732                     $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]])
1733                     if $selected_elements.length > 0
1734                         $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1735                     end
1736                 end
1737                 press_x = press_y = pos_x = pos_y = nil
1738                 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1739             end
1740         end
1741     }
1742     $autotable.signal_connect('motion-notify-event') { |w,e|
1743         if ! press_x.nil?
1744             if pos_x
1745                 $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]])
1746             end
1747             pos_x = e.x
1748             pos_y = e.y
1749             $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]])
1750             update_selected.call
1751         end
1752     }
1753
1754 end
1755
1756 def create_subalbums_page
1757
1758     subalbums_hb = Gtk::HBox.new
1759     $subalbums_vb = Gtk::VBox.new(false, 5)
1760     subalbums_hb.pack_start($subalbums_vb, false, false)
1761     $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1762     $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1763     $subalbums_sw.add_with_viewport(subalbums_hb)
1764 end
1765
1766 def save_current_file
1767     save_changes
1768
1769     if $filename
1770         begin
1771             begin
1772                 ios = File.open($filename, "w")
1773                 $xmldoc.write(ios, 0)
1774                 ios.close
1775             rescue Iconv::IllegalSequence
1776                 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1777                 if ! ios.nil? && ! ios.closed?
1778                     ios.close
1779                 end
1780                 $xmldoc.xml_decl.encoding = 'UTF-8'
1781                 ios = File.open($filename, "w")
1782                 $xmldoc.write(ios, 0)
1783                 ios.close
1784             end
1785             return true
1786         rescue Exception
1787             puts $!
1788             return false
1789         end
1790     end
1791 end
1792
1793 def save_current_file_user
1794     save_tempfilename = $filename
1795     $filename = $orig_filename
1796     if ! save_current_file
1797         show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1798         $filename = save_tempfilename
1799         return
1800     end
1801     $modified = false
1802     $generated_outofline = false
1803     $filename = save_tempfilename
1804
1805     msg 3, "performing actual deletion of: " + $todelete.join(', ')
1806     $todelete.each { |f|
1807         File.delete(f)
1808     }
1809 end
1810
1811 def mark_document_as_dirty
1812     $xmldoc.elements.each('//dir') { |elem|
1813         elem.delete_attribute('already-generated')
1814     }
1815 end
1816
1817 #- ret: true => ok  false => cancel
1818 def ask_save_modifications(msg1, msg2, *options)
1819     ret = true
1820     options = options.size > 0 ? options[0] : {}
1821     if $modified
1822         if options[:disallow_cancel]
1823             dialog = Gtk::Dialog.new(msg1,
1824                                      $main_window,
1825                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1826                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1827                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1828         else
1829             dialog = Gtk::Dialog.new(msg1,
1830                                      $main_window,
1831                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1832                                      [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1833                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1834                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1835         end
1836         dialog.default_response = Gtk::Dialog::RESPONSE_YES
1837         dialog.vbox.add(Gtk::Label.new(msg2))
1838         dialog.window_position = Gtk::Window::POS_CENTER
1839         dialog.show_all
1840         
1841         dialog.run { |response|
1842             dialog.destroy
1843             if response == Gtk::Dialog::RESPONSE_YES
1844                 if ! save_current_file_user
1845                     return ask_save_modifications(msg1, msg2, options)
1846                 end
1847             else
1848                 #- if we have generated an album but won't save modifications, we must remove 
1849                 #- already-generated markers in original file
1850                 if $generated_outofline
1851                     begin
1852                         $xmldoc = REXML::Document.new File.new($orig_filename)
1853                         mark_document_as_dirty
1854                         ios = File.open($orig_filename, "w")
1855                         $xmldoc.write(ios, 0)
1856                         ios.close
1857                     rescue Exception
1858                         puts "exception: #{$!}"
1859                     end
1860                 end
1861             end
1862             if response == Gtk::Dialog::RESPONSE_CANCEL
1863                 ret = false
1864             end
1865             $todelete = []  #- unconditionally clear the list of images/videos to delete
1866         }
1867     end
1868     return ret
1869 end
1870
1871 def try_quit(*options)
1872     if ask_save_modifications(utf8(_("Save before quitting?")),
1873                               utf8(_("Do you want to save your changes before quitting?")),
1874                               *options)
1875         Gtk.main_quit
1876     end
1877 end
1878
1879 def show_popup(parent, msg, *options)
1880     dialog = Gtk::Dialog.new
1881     if options[0] && options[0][:title]
1882         dialog.title = options[0][:title]
1883     else
1884         dialog.title = utf8(_("Booh message"))
1885     end
1886     lbl = Gtk::Label.new
1887     if options[0] && options[0][:nomarkup]
1888         lbl.text = msg
1889     else
1890         lbl.markup = msg
1891     end
1892     if options[0] && options[0][:centered]
1893         lbl.set_justify(Gtk::Justification::CENTER)
1894     end
1895     if options[0] && options[0][:selectable]
1896         lbl.selectable = true
1897     end
1898     if options[0] && options[0][:topwidget]
1899         dialog.vbox.add(options[0][:topwidget])
1900     end
1901     if options[0] && options[0][:scrolled]
1902         sw = Gtk::ScrolledWindow.new(nil, nil)
1903         sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1904         sw.add_with_viewport(lbl)
1905         dialog.vbox.add(sw)
1906         dialog.set_default_size(500, 600)
1907     else
1908         dialog.vbox.add(lbl)
1909         dialog.set_default_size(200, 120)
1910     end
1911     if options[0] && options[0][:okcancel]
1912         dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1913     end
1914     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1915
1916     if options[0] && options[0][:pos_centered]
1917         dialog.window_position = Gtk::Window::POS_CENTER
1918     else
1919         dialog.window_position = Gtk::Window::POS_MOUSE
1920     end
1921
1922     if options[0] && options[0][:linkurl]
1923         linkbut = Gtk::Button.new('')
1924         linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1925         linkbut.signal_connect('clicked') {
1926             open_url(options[0][:linkurl])
1927             dialog.response(Gtk::Dialog::RESPONSE_OK)
1928             set_mousecursor_normal
1929         }
1930         linkbut.relief = Gtk::RELIEF_NONE
1931         linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1932         linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1933         dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1934     end
1935
1936     dialog.show_all
1937
1938     if !options[0] || !options[0][:not_transient]
1939         dialog.transient_for = parent
1940         dialog.run { |response|
1941             dialog.destroy
1942             if options[0] && options[0][:okcancel]
1943                 return response == Gtk::Dialog::RESPONSE_OK
1944             end
1945         }
1946     else
1947         dialog.signal_connect('response') { dialog.destroy }
1948     end
1949 end
1950
1951 def backend_wait_message(parent, msg, infopipe_path, mode)
1952     w = Gtk::Window.new
1953     w.set_transient_for(parent)
1954     w.modal = true
1955
1956     vb = Gtk::VBox.new(false, 5).set_border_width(5)
1957     vb.pack_start(Gtk::Label.new(msg), false, false)
1958
1959     vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1960     vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1961     if mode != 'one dir scan'
1962         vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1963     end
1964     if mode == 'web-album'
1965         vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1966         vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1967     end
1968     vb.pack_start(Gtk::HSeparator.new, false, false)
1969
1970     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1971     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1972     vb.pack_end(bottom, false, false)
1973
1974     infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1975     refresh_thread = Thread.new {
1976         directories_counter = 0
1977         while line = infopipe.gets
1978             if line =~ /^directories: (\d+), sizes: (\d+)/
1979                 directories = $1.to_f + 1
1980                 sizes = $2.to_f
1981             elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1982                 elements = $3.to_f + 1
1983                 if mode == 'web-album'
1984                     elements += sizes
1985                 end
1986                 element_counter = 0
1987                 gtk_thread_protect { pb1_1.fraction = 0 }
1988                 if mode != 'one dir scan'
1989                     newtext = utf8(full_src_dir_to_rel($1, $2))
1990                     newtext = '/' if newtext == ''
1991                     gtk_thread_protect { pb1_2.text = newtext }
1992                     directories_counter += 1
1993                     gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1994                 end
1995             elsif line =~ /^processing element$/
1996                 element_counter += 1
1997                 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1998             elsif line =~ /^processing size$/
1999                 element_counter += 1
2000                 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
2001             elsif line =~ /^finished processing sizes$/
2002                 gtk_thread_protect { pb1_1.fraction = 1 }
2003             elsif line =~ /^creating index.html$/
2004                 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2005                 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2006                 directories_counter = 0
2007             elsif line =~ /^index.html: (.+)\|(.+)/
2008                 newtext = utf8(full_src_dir_to_rel($1, $2))
2009                 newtext = '/' if newtext == ''
2010                 gtk_thread_protect { pb2.text = newtext }
2011                 directories_counter += 1
2012                 gtk_thread_protect { pb2.fraction = directories_counter / directories }
2013             elsif line =~ /^die: (.*)$/
2014                 $diemsg = $1
2015             end
2016         end
2017     }
2018
2019     w.add(vb)
2020     w.signal_connect('delete-event') { w.destroy }
2021     w.signal_connect('destroy') {
2022         Thread.kill(refresh_thread)
2023         gtk_thread_flush  #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2024         if infopipe_path
2025             infopipe.close
2026             File.delete(infopipe_path)
2027         end
2028     }
2029     w.window_position = Gtk::Window::POS_CENTER
2030     w.show_all
2031
2032     return [ b, w ]
2033 end
2034
2035 def call_backend(cmd, waitmsg, mode, params)
2036     pipe = Tempfile.new("boohpipe")
2037     pipe.close!
2038     system("mkfifo #{pipe.path}")
2039     cmd += " --info-pipe #{pipe.path}"
2040     button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2041     pid = nil
2042     Thread.new {
2043         msg 2, cmd
2044         if pid = fork
2045             id, exitstatus = Process.waitpid2(pid)
2046             gtk_thread_protect { w8.destroy }
2047             if exitstatus == 0
2048                 if params[:successmsg]
2049                     gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2050                 end
2051                 if params[:closure_after]
2052                     gtk_thread_protect(&params[:closure_after])
2053                 end
2054             elsif exitstatus == 15
2055                 #- say nothing, user aborted
2056             else
2057                 gtk_thread_protect { show_popup($main_window,
2058                                                 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2059             end
2060         else
2061             exec(cmd)
2062         end
2063     }
2064     button.signal_connect('clicked') {
2065         Process.kill('SIGTERM', pid)
2066     }
2067 end
2068
2069 def save_changes(*forced)
2070     if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2071         return
2072     end
2073
2074     $xmldir.delete_attribute('already-generated')
2075
2076     propagate_children = proc { |xmldir|
2077         if xmldir.attributes['subdirs-caption']
2078             xmldir.delete_attribute('already-generated')
2079         end
2080         xmldir.elements.each('dir') { |element|
2081             propagate_children.call(element)
2082         }
2083     }
2084
2085     if $xmldir.child_byname_notattr('dir', 'deleted')
2086         new_title = $subalbums_title.buffer.text
2087         if new_title != $xmldir.attributes['subdirs-caption']
2088             parent = $xmldir.parent
2089             if parent.name == 'dir'
2090                 parent.delete_attribute('already-generated')
2091             end
2092             propagate_children.call($xmldir)
2093         end
2094         $xmldir.add_attribute('subdirs-caption', new_title)
2095         $xmldir.elements.each('dir') { |element|
2096             if !element.attributes['deleted']
2097                 path = element.attributes['path']
2098                 newtext = $subalbums_edits[path][:editzone].buffer.text
2099                 if element.attributes['subdirs-caption']
2100                     if element.attributes['subdirs-caption'] != newtext
2101                         propagate_children.call(element)
2102                     end
2103                     element.add_attribute('subdirs-caption',     newtext)
2104                     element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2105                 else
2106                     if element.attributes['thumbnails-caption'] != newtext
2107                         element.delete_attribute('already-generated')
2108                     end
2109                     element.add_attribute('thumbnails-caption',     newtext)
2110                     element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2111                 end
2112             end
2113         }
2114     end
2115
2116     if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2117         if $xmldir.attributes['thumbnails-caption']
2118             path = $xmldir.attributes['path']
2119             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2120         end
2121     elsif $xmldir.attributes['thumbnails-caption']
2122         $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2123     end
2124
2125     if $xmldir.attributes['thumbnails-caption']
2126         if edit = $subalbums_edits[$xmldir.attributes['path']]
2127             $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2128         end
2129     end
2130
2131     #- remove and reinsert elements to reflect new ordering
2132     saves = {}
2133     cpt = 0
2134     $xmldir.elements.each { |element|
2135         if element.name == 'image' || element.name == 'video'
2136             saves[element.attributes['filename']] = element.remove
2137             cpt += 1
2138         end
2139     }
2140     $autotable.current_order.each { |path|
2141         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2142         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2143         saves.delete(path)
2144     }
2145     saves.each_key { |path|
2146         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2147         chld.add_attribute('deleted', 'true')
2148     }
2149 end
2150
2151 def sort_by_exif_date
2152     $modified = true
2153     save_changes
2154     current_order = []
2155     $xmldir.elements.each { |element|
2156         if element.name == 'image' || element.name == 'video'
2157             current_order << element.attributes['filename']
2158         end
2159     }
2160
2161     #- look for EXIF dates
2162     dates = {}
2163
2164     if current_order.size > 20
2165         w = Gtk::Window.new
2166         w.set_transient_for($main_window)
2167         w.modal = true
2168         vb = Gtk::VBox.new(false, 5).set_border_width(5)
2169         vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2170         vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2171         bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2172         b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2173         vb.pack_end(bottom, false, false)
2174         w.add(vb)
2175         w.signal_connect('delete-event') { w.destroy }
2176         w.window_position = Gtk::Window::POS_CENTER
2177         w.show_all
2178
2179         aborted = false
2180         b.signal_connect('clicked') { aborted = true }
2181         i = 0
2182         current_order.each { |f|
2183             i += 1
2184             if entry2type(f) == 'image'
2185                 pb.text = f
2186                 pb.fraction = i.to_f / current_order.size
2187                 Gtk.main_iteration while Gtk.events_pending?
2188                 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2189                 if ! date_time.nil?
2190                     dates[f] = date_time
2191                 end
2192             end
2193             if aborted
2194                 break
2195             end
2196         }
2197         w.destroy
2198         if aborted
2199             return
2200         end
2201
2202     else
2203         current_order.each { |f|
2204             date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2205             if ! date_time.nil?
2206                 dates[f] = date_time
2207             end
2208         }
2209     end
2210
2211     saves = {}
2212     $xmldir.elements.each { |element|
2213         if element.name == 'image' || element.name == 'video'
2214             saves[element.attributes['filename']] = element.remove
2215         end
2216     }
2217
2218     neworder = smartsort(current_order, dates)
2219
2220     neworder.each { |f|
2221         $xmldir.add_element(saves[f].name, saves[f].attributes)
2222     }
2223
2224     #- let the auto-table reflect new ordering
2225     change_dir
2226 end
2227
2228 def remove_all_captions
2229     $modified = true
2230     texts = {}
2231     $autotable.current_order.each { |path|
2232         texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2233         $name2widgets[File.basename(path)][:textview].buffer.text = ''
2234     }
2235     save_undo(_("remove all captions"),
2236               proc { |texts|
2237                   texts.each_key { |key|
2238                       $name2widgets[key][:textview].buffer.text = texts[key]
2239                   }
2240                   $notebook.set_page(1)
2241                   proc {
2242                       texts.each_key { |key|
2243                           $name2widgets[key][:textview].buffer.text = ''
2244                       }
2245                       $notebook.set_page(1)
2246                   }
2247               }, texts)
2248 end
2249
2250 def change_dir
2251     $selected_elements.each_key { |path|
2252         $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2253     }
2254     $autotable.clear
2255     $vbox2widgets = {}
2256     $name2widgets = {}
2257     $name2closures = {}
2258     $selected_elements = {}
2259     $cuts = []
2260     $multiple_dnd = []
2261     UndoHandler.cleanup
2262     $undo_tb.sensitive = $undo_mb.sensitive = false
2263     $redo_tb.sensitive = $redo_mb.sensitive = false
2264
2265     if !$current_path
2266         return
2267     end
2268
2269     $subalbums_vb.children.each { |chld|
2270         $subalbums_vb.remove(chld)
2271     }
2272     $subalbums = Gtk::Table.new(0, 0, true)
2273     current_y_sub_albums = 0
2274
2275     $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"])
2276     $subalbums_edits = {}
2277     subalbums_counter = 0
2278     subalbums_edits_bypos = {}
2279
2280     add_subalbum = proc { |xmldir, counter|
2281         $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2282         subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2283         if xmldir == $xmldir
2284             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2285             captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2286             caption = xmldir.attributes['thumbnails-caption']
2287             infotype = 'thumbnails'
2288         else
2289             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2290             captionfile, caption = find_subalbum_caption_info(xmldir)
2291             infotype = find_subalbum_info_type(xmldir)
2292         end
2293         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2294         hbox = Gtk::HBox.new
2295         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2296         f = Gtk::Frame.new
2297         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2298
2299         img = nil
2300         my_gen_real_thumbnail = proc {
2301             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2302         }
2303
2304         if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2305             f.add(img = Gtk::Image.new)
2306             my_gen_real_thumbnail.call
2307         else
2308             f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2309         end
2310         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2311         $subalbums.attach(hbox,
2312                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2313
2314         frame, textview = create_editzone($subalbums_sw, 0, img)
2315         textview.buffer.text = caption
2316         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2317                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2318
2319         change_image = proc {
2320             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2321                                             nil,
2322                                             Gtk::FileChooser::ACTION_OPEN,
2323                                             nil,
2324                                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2325             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2326             fc.transient_for = $main_window
2327             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))
2328             f.add(preview_img = Gtk::Image.new)
2329             preview.show_all
2330             fc.signal_connect('update-preview') { |w|
2331                 begin
2332                     if fc.preview_filename
2333                         preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2334                         fc.preview_widget_active = true
2335                     end
2336                 rescue Gdk::PixbufError
2337                     fc.preview_widget_active = false
2338                 end
2339             }
2340             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2341                 $modified = true
2342                 old_file = captionfile
2343                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2344                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2345                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2346                 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2347
2348                 new_file = fc.filename
2349                 msg 3, "new captionfile is: #{fc.filename}"
2350                 perform_changefile = proc {
2351                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2352                     $modified_pixbufs.delete(thumbnail_file)
2353                     xmldir.delete_attribute("#{infotype}-rotate")
2354                     xmldir.delete_attribute("#{infotype}-color-swap")
2355                     xmldir.delete_attribute("#{infotype}-enhance")
2356                     xmldir.delete_attribute("#{infotype}-seektime")
2357                     my_gen_real_thumbnail.call
2358                 }
2359                 perform_changefile.call
2360
2361                 save_undo(_("change caption file for sub-album"),
2362                           proc {
2363                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2364                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2365                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2366                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2367                               xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2368                               my_gen_real_thumbnail.call
2369                               $notebook.set_page(0)
2370                               proc {
2371                                   perform_changefile.call
2372                                   $notebook.set_page(0)
2373                               }
2374                           })
2375             end
2376             fc.destroy
2377         }
2378
2379         refresh = proc {
2380             if File.exists?(thumbnail_file)
2381                 File.delete(thumbnail_file)
2382             end
2383             my_gen_real_thumbnail.call
2384         }
2385
2386         rotate_and_cleanup = proc { |angle|
2387             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2388             if File.exists?(thumbnail_file)
2389                 File.delete(thumbnail_file)
2390             end
2391         }
2392
2393         move = proc { |direction|
2394             $modified = true
2395
2396             save_changes('forced')
2397             oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2398             if direction == 'up'
2399                 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2400                 subalbums_edits_bypos[oldpos - 1][:position] += 1
2401             end
2402             if direction == 'down'
2403                 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2404                 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2405             end
2406             if direction == 'top'
2407                 for i in 1 .. oldpos - 1
2408                     subalbums_edits_bypos[i][:position] += 1
2409                 end
2410                 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2411             end
2412             if direction == 'bottom'
2413                 for i in oldpos + 1 .. subalbums_counter
2414                     subalbums_edits_bypos[i][:position] -= 1
2415                 end
2416                 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2417             end
2418
2419             elems = []
2420             $xmldir.elements.each('dir') { |element|
2421                 if (!element.attributes['deleted'])
2422                     elems << [ element.attributes['path'], element.remove ]
2423                 end
2424             }
2425             elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2426                   each { |e| $xmldir.add_element(e[1]) }
2427             #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2428             $xmldir.elements.each('descendant::dir') { |elem|
2429                 elem.delete_attribute('already-generated')
2430             }
2431
2432             sel = $albums_tv.selection.selected_rows
2433             change_dir
2434             populate_subalbums_treeview(false)
2435             $albums_tv.selection.select_path(sel[0])
2436         }
2437
2438         color_swap_and_cleanup = proc {
2439             perform_color_swap_and_cleanup = proc {
2440                 color_swap(xmldir, "#{infotype}-")
2441                 my_gen_real_thumbnail.call
2442             }
2443             perform_color_swap_and_cleanup.call
2444
2445             save_undo(_("color swap"),
2446                       proc {
2447                           perform_color_swap_and_cleanup.call
2448                           $notebook.set_page(0)
2449                           proc {
2450                               perform_color_swap_and_cleanup.call
2451                               $notebook.set_page(0)
2452                           }
2453                       })
2454         }
2455
2456         change_seektime_and_cleanup = proc {
2457             if values = ask_new_seektime(xmldir, "#{infotype}-")
2458                 perform_change_seektime_and_cleanup = proc { |val|
2459                     change_seektime(xmldir, "#{infotype}-", val)
2460                     my_gen_real_thumbnail.call
2461                 }
2462                 perform_change_seektime_and_cleanup.call(values[:new])
2463
2464                 save_undo(_("specify seektime"),
2465                           proc {
2466                               perform_change_seektime_and_cleanup.call(values[:old])
2467                               $notebook.set_page(0)
2468                               proc {
2469                                   perform_change_seektime_and_cleanup.call(values[:new])
2470                                   $notebook.set_page(0)
2471                               }
2472                           })
2473             end
2474         }
2475
2476         whitebalance_and_cleanup = proc {
2477             if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2478                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2479                 perform_change_whitebalance_and_cleanup = proc { |val|
2480                     change_whitebalance(xmldir, "#{infotype}-", val)
2481                     recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2482                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2483                     if File.exists?(thumbnail_file)
2484                         File.delete(thumbnail_file)
2485                     end
2486                 }
2487                 perform_change_whitebalance_and_cleanup.call(values[:new])
2488                 
2489                 save_undo(_("fix white balance"),
2490                           proc {
2491                               perform_change_whitebalance_and_cleanup.call(values[:old])
2492                               $notebook.set_page(0)
2493                               proc {
2494                                   perform_change_whitebalance_and_cleanup.call(values[:new])
2495                                   $notebook.set_page(0)
2496                               }
2497                           })
2498             end
2499         }
2500
2501         gammacorrect_and_cleanup = proc {
2502             if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2503                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2504                 perform_change_gammacorrect_and_cleanup = proc { |val|
2505                     change_gammacorrect(xmldir, "#{infotype}-", val)
2506                     recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2507                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2508                     if File.exists?(thumbnail_file)
2509                         File.delete(thumbnail_file)
2510                     end
2511                 }
2512                 perform_change_gammacorrect_and_cleanup.call(values[:new])
2513                 
2514                 save_undo(_("gamma correction"),
2515                           proc {
2516                               perform_change_gammacorrect_and_cleanup.call(values[:old])
2517                               $notebook.set_page(0)
2518                               proc {
2519                                   perform_change_gammacorrect_and_cleanup.call(values[:new])
2520                                   $notebook.set_page(0)
2521                               }
2522                           })
2523             end
2524         }
2525
2526         enhance_and_cleanup = proc {
2527             perform_enhance_and_cleanup = proc {
2528                 enhance(xmldir, "#{infotype}-")
2529                 my_gen_real_thumbnail.call
2530             }
2531             
2532             perform_enhance_and_cleanup.call
2533             
2534             save_undo(_("enhance"),
2535                       proc {
2536                           perform_enhance_and_cleanup.call
2537                           $notebook.set_page(0)
2538                           proc {
2539                               perform_enhance_and_cleanup.call
2540                               $notebook.set_page(0)
2541                           }
2542                       })
2543         }
2544
2545         evtbox.signal_connect('button-press-event') { |w, event|
2546             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2547                 if $r90.active?
2548                     rotate_and_cleanup.call(90)
2549                 elsif $r270.active?
2550                     rotate_and_cleanup.call(-90)
2551                 elsif $enhance.active?
2552                     enhance_and_cleanup.call
2553                 end
2554             end
2555             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2556                 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2557                                      { :forbid_left => true, :forbid_right => true,
2558                                        :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2559                                        :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2560                                      { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2561                                        :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2562                                        :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2563             end
2564             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2565                 change_image.call
2566                 true   #- handled
2567             end
2568         }
2569         evtbox.signal_connect('button-press-event') { |w, event|
2570             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2571             false
2572         }
2573
2574         evtbox.signal_connect('button-release-event') { |w, event|
2575             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2576                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2577                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2578                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2579                     msg 3, "gesture rotate: #{angle}"
2580                     rotate_and_cleanup.call(angle)
2581                 end
2582             end
2583             $gesture_press = nil
2584         }
2585                 
2586         $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2587         $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2588         current_y_sub_albums += 1
2589     }
2590
2591     if $xmldir.child_byname_notattr('dir', 'deleted')
2592         #- title edition
2593         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2594         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2595         $subalbums_title.set_justification(Gtk::Justification::CENTER)
2596         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2597         #- this album image/caption
2598         if $xmldir.attributes['thumbnails-caption']
2599             add_subalbum.call($xmldir, 0)
2600         end
2601     end
2602     total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2603     $xmldir.elements.each { |element|
2604         if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2605             #- element (image or video) of this album
2606             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2607             msg 3, "dest_img: #{dest_img}"
2608             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2609             total[element.name] += 1
2610         end
2611         if element.name == 'dir' && !element.attributes['deleted']
2612             #- sub-album image/caption
2613             add_subalbum.call(element, subalbums_counter += 1)
2614             total[element.name] += 1
2615         end
2616     }
2617     $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2618                                                                                 total['image'], total['video'], total['dir'] ]))
2619     $subalbums_vb.add($subalbums)
2620     $subalbums_vb.show_all
2621
2622     if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2623         $notebook.get_tab_label($autotable_sw).sensitive = false
2624         $notebook.set_page(0)
2625         $thumbnails_title.buffer.text = ''
2626     else
2627         $notebook.get_tab_label($autotable_sw).sensitive = true
2628         $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2629     end
2630
2631     if !$xmldir.child_byname_notattr('dir', 'deleted')
2632         $notebook.get_tab_label($subalbums_sw).sensitive = false
2633         $notebook.set_page(1)
2634     else
2635         $notebook.get_tab_label($subalbums_sw).sensitive = true
2636     end
2637 end
2638
2639 def pixbuf_or_nil(filename)
2640     begin
2641         return Gdk::Pixbuf.new(filename)
2642     rescue
2643         return nil
2644     end
2645 end
2646
2647 def theme_choose(current)
2648     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2649                              $main_window,
2650                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2651                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2652                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2653
2654     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2655     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2656     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2657     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2658     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2659     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2660     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2661     treeview.signal_connect('button-press-event') { |w, event|
2662         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2663             dialog.response(Gtk::Dialog::RESPONSE_OK)
2664         end
2665     }
2666
2667     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2668
2669     `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2670         dir.chomp!
2671         iter = model.append
2672         iter[0] = File.basename(dir)
2673         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2674         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2675         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2676         if File.basename(dir) == current
2677             treeview.selection.select_iter(iter)
2678         end
2679     }
2680
2681     dialog.set_default_size(700, 400)
2682     dialog.vbox.show_all
2683     dialog.run { |response|
2684         iter = treeview.selection.selected
2685         dialog.destroy
2686         if response == Gtk::Dialog::RESPONSE_OK && iter
2687             return model.get_value(iter, 0)
2688         end
2689     }
2690     return nil
2691 end
2692
2693 def show_password_protections
2694     examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2695         child_iter = $albums_iters[xmldir.attributes['path']]
2696         if xmldir.attributes['password-protect']
2697             child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2698             already_protected = true
2699         elsif already_protected
2700             pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2701             if pix
2702                 pix = pix.saturate_and_pixelate(1, true)
2703             end
2704             child_iter[2] = pix
2705         else
2706             child_iter[2] = nil
2707         end
2708         xmldir.elements.each('dir') { |elem|
2709             if !elem.attributes['deleted']
2710                 examine_dir_elem.call(child_iter, elem, already_protected)
2711             end
2712         }
2713     }
2714     examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2715 end
2716
2717 def populate_subalbums_treeview(select_first)
2718     $albums_ts.clear
2719     $autotable.clear
2720     $albums_iters = {}
2721     $subalbums_vb.children.each { |chld|
2722         $subalbums_vb.remove(chld)
2723     }
2724
2725     source = $xmldoc.root.attributes['source']
2726     msg 3, "source: #{source}"
2727
2728     xmldir = $xmldoc.elements['//dir']
2729     if !xmldir || xmldir.attributes['path'] != source
2730         msg 1, _("Corrupted booh file...")
2731         return
2732     end
2733
2734     append_dir_elem = proc { |parent_iter, xmldir|
2735         child_iter = $albums_ts.append(parent_iter)
2736         child_iter[0] = File.basename(xmldir.attributes['path'])
2737         child_iter[1] = xmldir.attributes['path']
2738         $albums_iters[xmldir.attributes['path']] = child_iter
2739         msg 3, "puttin location: #{xmldir.attributes['path']}"
2740         xmldir.elements.each('dir') { |elem|
2741             if !elem.attributes['deleted']
2742                 append_dir_elem.call(child_iter, elem)
2743             end
2744         }
2745     }
2746     append_dir_elem.call(nil, xmldir)
2747     show_password_protections
2748
2749     $albums_tv.expand_all
2750     if select_first
2751         $albums_tv.selection.select_iter($albums_ts.iter_first)
2752     end
2753 end
2754
2755 def select_current_theme
2756     select_theme($xmldoc.root.attributes['theme'],
2757                  $xmldoc.root.attributes['limit-sizes'],
2758                  !$xmldoc.root.attributes['optimize-for-32'].nil?,
2759                  $xmldoc.root.attributes['thumbnails-per-row'])
2760 end
2761
2762 def open_file(filename)
2763
2764     $filename = nil
2765     $modified = false
2766     $current_path = nil   #- invalidate
2767     $modified_pixbufs = {}
2768     $albums_ts.clear
2769     $autotable.clear
2770     $subalbums_vb.children.each { |chld|
2771         $subalbums_vb.remove(chld)
2772     }
2773
2774     if !File.exists?(filename)
2775         return utf8(_("File not found."))
2776     end
2777
2778     begin
2779         $xmldoc = REXML::Document.new File.new(filename)
2780     rescue Exception
2781         $xmldoc = nil
2782     end
2783
2784     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2785         if entry2type(filename).nil?
2786             return utf8(_("Not a booh file!"))
2787         else
2788             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."))
2789         end
2790     end
2791
2792     if !source = $xmldoc.root.attributes['source']
2793         return utf8(_("Corrupted booh file..."))
2794     end
2795
2796     if !dest = $xmldoc.root.attributes['destination']
2797         return utf8(_("Corrupted booh file..."))
2798     end
2799
2800     if !theme = $xmldoc.root.attributes['theme']
2801         return utf8(_("Corrupted booh file..."))
2802     end
2803
2804     if $xmldoc.root.attributes['version'] < '0.8.99.2'
2805         msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2806         mark_document_as_dirty
2807         if $xmldoc.root.attributes['version'] < '0.8.4'
2808             msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2809             `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2810                 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2811                 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2812                 if old_dest_dir != new_dest_dir
2813                     sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2814                 end
2815                 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2816                     xmldir.elements.each { |element|
2817                         if %w(image video).include?(element.name) && !element.attributes['deleted']
2818                             old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2819                             new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2820                             Dir[old_name + '*'].each { |file|
2821                                 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2822                                 file != new_file and sys("mv '#{file}' '#{new_file}'")
2823                             }
2824                         end
2825                         if element.name == 'dir' && !element.attributes['deleted']
2826                             old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2827                             new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2828                             old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2829                         end
2830                     }
2831                 else
2832                     msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2833                 end
2834             }
2835         end
2836         $xmldoc.root.add_attribute('version', $VERSION)
2837     end
2838
2839     select_current_theme
2840
2841     $filename = filename
2842     $default_size['thumbnails'] =~ /(.*)x(.*)/
2843     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2844     $albums_thumbnail_size =~ /(.*)x(.*)/
2845     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2846
2847     populate_subalbums_treeview(true)
2848
2849     $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
2850     return nil
2851 end
2852
2853 def open_file_user(filename)
2854     result = open_file(filename)
2855     if !result
2856         $config['last-opens'] ||= []
2857         if $config['last-opens'][-1] != utf8(filename)
2858             $config['last-opens'] << utf8(filename)
2859         end
2860         $orig_filename = $filename
2861         tmp = Tempfile.new("boohtemp")
2862         tmp.close!
2863         #- for security
2864         ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2865         ios.close
2866         $tempfiles << $filename << "#{$filename}.backup"
2867     else
2868         $orig_filename = nil
2869     end
2870     return result
2871 end
2872
2873 def open_file_popup
2874     if !ask_save_modifications(utf8(_("Save this album?")),
2875                                utf8(_("Do you want to save the changes to this album?")),
2876                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2877         return
2878     end
2879     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2880                                     nil,
2881                                     Gtk::FileChooser::ACTION_OPEN,
2882                                     nil,
2883                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2884     fc.add_shortcut_folder(File.expand_path("~/.booh"))
2885     fc.set_current_folder(File.expand_path("~/.booh"))
2886     fc.transient_for = $main_window
2887     ok = false
2888     while !ok
2889         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2890             push_mousecursor_wait(fc)
2891             msg = open_file_user(fc.filename)
2892             pop_mousecursor(fc)
2893             if msg
2894                 show_popup(fc, msg)
2895                 ok = false
2896             else
2897                 ok = true
2898             end
2899         else
2900             ok = true
2901         end
2902     end
2903     fc.destroy
2904 end
2905
2906 def additional_booh_options
2907     options = ''
2908     if $config['mproc']
2909         options += "--mproc #{$config['mproc'].to_i} "
2910     end
2911     options += "--comments-format '#{$config['comments-format']}'"
2912     return options
2913 end
2914
2915 def ask_multi_languages(value)
2916     if ! value.nil?
2917         spl = value.split(',')
2918         value = [ spl[0..-2], spl[-1] ]
2919     end
2920
2921     dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
2922                              $main_window,
2923                              Gtk::Dialog::MODAL,
2924                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2925                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2926
2927     lbl = Gtk::Label.new
2928     lbl.markup = utf8(
2929 _("You can choose to activate <b>multi-languages</b> support for this web-album
2930 (it will work only if you publish your web-album on an Apache web-server). This will
2931 use the MultiViews feature of Apache; the pages will be served according to the
2932 value of the Accept-Language HTTP header sent by the web browsers, so that people
2933 with different languages preferences will be able to browse your web-album with
2934 navigation in their language (if language is available).
2935 "))
2936
2937     dialog.vbox.add(lbl)
2938     dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
2939                                                                          add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
2940                                                                                                      add(languages = Gtk::Button.new))))
2941
2942     pick_languages = proc {
2943         dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
2944                                   $main_window,
2945                                   Gtk::Dialog::MODAL,
2946                                   [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2947                                   [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2948
2949         dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
2950         hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
2951         cbs = []
2952         SUPPORTED_LANGUAGES.each { |lang|
2953             hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
2954             if ! value.nil? && value[0].include?(lang)
2955                 cb.active = true
2956             end
2957             cbs << [ lang, cb ]
2958         }
2959
2960         dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
2961         hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
2962         fallback_language = nil
2963         hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
2964         fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
2965         if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
2966             fbl_rb.active = true
2967             fallback_language = SUPPORTED_LANGUAGES[0]
2968         end
2969         SUPPORTED_LANGUAGES[1..-1].each { |lang|
2970             hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
2971             rb.signal_connect('clicked') { fallback_language = lang }
2972             if ! value.nil? && value[1] == lang
2973                 rb.active = true
2974             end
2975         }
2976
2977         dialog2.window_position = Gtk::Window::POS_MOUSE
2978         dialog2.show_all
2979
2980         resp = nil
2981         dialog2.run { |response|
2982             resp = response
2983             if resp == Gtk::Dialog::RESPONSE_OK
2984                 value = []
2985                 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
2986                 value[1] = fallback_language
2987                 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| utf8(langname(v)) }.join(', '), utf8(langname(value[1])) ])
2988             end
2989             dialog2.destroy
2990         }
2991         resp
2992     }
2993
2994     languages.signal_connect('clicked') {
2995         pick_languages.call
2996     }
2997     dialog.window_position = Gtk::Window::POS_MOUSE
2998     if value.nil?
2999         rb_no.active = true
3000     else
3001         rb_yes.active = true
3002         languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| utf8(langname(v)) }.join(', '), utf8(langname(value[1])) ])
3003     end
3004     rb_no.signal_connect('clicked') {
3005         if rb_no.active?
3006             languages.hide
3007         else
3008             if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3009                 rb_no.activate
3010             else
3011                 languages.show
3012             end
3013         end
3014     }
3015     oldval = value
3016     dialog.show_all
3017     if rb_no.active?
3018         languages.hide
3019     end
3020
3021     dialog.run { |response|
3022         if rb_no.active?
3023             value = nil
3024         end
3025         dialog.destroy
3026         if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3027             if value.nil?
3028                 return [ true, nil ]
3029             else
3030                 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3031             end
3032         else
3033             return [ false ]
3034         end
3035     }
3036 end
3037
3038 def new_album
3039     if !ask_save_modifications(utf8(_("Save this album?")),
3040                                utf8(_("Do you want to save the changes to this album?")),
3041                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3042         return
3043     end
3044     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3045                              $main_window,
3046                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3047                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3048                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3049     
3050     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3051     tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
3052                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3053     tbl.attach(src = Gtk::Entry.new,
3054                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3055     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3056                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3057     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
3058                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3059     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3060                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3061     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3062                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3063     tbl.attach(dest = Gtk::Entry.new,
3064                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3065     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3066                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3067     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3068                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3069     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3070                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3071     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3072                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3073
3074     tooltips = Gtk::Tooltips.new
3075     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3076     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3077                          pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3078     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3079                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3080     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
3081     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)
3082     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3083                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3084     nperpage_model = Gtk::ListStore.new(String, String)
3085     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3086                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3087     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3088     nperpagecombo.set_attributes(crt, { :markup => 0 })
3089     iter = nperpage_model.append
3090     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3091     iter[1] = nil
3092     [ 12, 20, 30, 40, 50 ].each { |v|
3093         iter = nperpage_model.append
3094         iter[0] = iter[1] = v.to_s
3095     }
3096     nperpagecombo.active = 0
3097
3098     multilanguages_value = nil
3099     vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new(utf8(_("Multi-languages: disabled."))), false, false, 0).
3100                                         pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3101     tooltips.set_tip(ml, utf8(_("When disabled, the web-album will be generated with navigation in your desktop language. When enabled, the web-album will be generated with navigation in all languages you select, but you have to publish your web-album on an Apache web-server for that feature to work.")), nil)
3102     multilanguages.signal_connect('clicked') {
3103         retval = ask_multi_languages(multilanguages_value)
3104         if retval[0] 
3105             multilanguages_value = retval[1]
3106         end
3107         if multilanguages_value
3108             ml_label.text = utf8(_("Multi-languages: enabled."))
3109         else
3110             ml_label.text = utf8(_("Multi-languages: disabled."))
3111         end
3112     }
3113
3114     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3115                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3116     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)
3117     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3118                                    pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
3119     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)
3120
3121     src_nb_calculated_for = ''
3122     src_nb_thread = nil
3123     process_src_nb = proc {
3124         if src.text != src_nb_calculated_for
3125             src_nb_calculated_for = src.text
3126             if src_nb_thread
3127                 Thread.kill(src_nb_thread)
3128                 src_nb_thread = nil
3129             end
3130             if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3131                 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3132             else
3133                 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3134                     if File.readable?(from_utf8_safe(src_nb_calculated_for))
3135                         src_nb_thread = Thread.new {
3136                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3137                             total = { 'image' => 0, 'video' => 0, nil => 0 }
3138                             `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3139                                 if File.basename(dir) =~ /^\./
3140                                     next
3141                                 else
3142                                     begin
3143                                         Dir.entries(dir.chomp).each { |file|
3144                                             total[entry2type(file)] += 1
3145                                         }
3146                                     rescue Errno::EACCES, Errno::ENOENT
3147                                     end
3148                                 end
3149                             }
3150                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3151                             src_nb_thread = nil
3152                         }
3153                     else
3154                         src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3155                     end
3156                 else
3157                     src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3158                 end
3159             end
3160         end
3161         true
3162     }
3163     timeout_src_nb = Gtk.timeout_add(100) {
3164         process_src_nb.call
3165     }
3166
3167     src_browse.signal_connect('clicked') {
3168         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
3169                                         nil,
3170                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
3171                                         nil,
3172                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3173         fc.transient_for = $main_window
3174         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3175             src.text = utf8(fc.filename)
3176             process_src_nb.call
3177             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3178         end
3179         fc.destroy
3180     }
3181
3182     dest_browse.signal_connect('clicked') {
3183         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3184                                         nil,
3185                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
3186                                         nil,
3187                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3188         fc.transient_for = $main_window
3189         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3190             dest.text = utf8(fc.filename)
3191         end
3192         fc.destroy
3193     }
3194
3195     conf_browse.signal_connect('clicked') {
3196         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3197                                         nil,
3198                                         Gtk::FileChooser::ACTION_SAVE,
3199                                         nil,
3200                                         [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3201         fc.transient_for = $main_window
3202         fc.add_shortcut_folder(File.expand_path("~/.booh"))
3203         fc.set_current_folder(File.expand_path("~/.booh"))
3204         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3205             conf.text = utf8(fc.filename)
3206         end
3207         fc.destroy
3208     }
3209
3210     theme_sizes = []
3211     nperrows = []
3212     recreate_theme_config = proc {
3213         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3214         theme_sizes = []
3215         select_theme(theme_button.label, 'all', optimize432.active?, nil)
3216         $images_size.each { |s|
3217             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3218             if !s['optional']
3219                 cb.active = true
3220             end
3221             tooltips.set_tip(cb, utf8(s['description']), nil)
3222             theme_sizes << { :widget => cb, :value => s['name'] }
3223         }
3224         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3225         tooltips = Gtk::Tooltips.new
3226         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3227         theme_sizes << { :widget => cb, :value => 'original' }
3228         sizes.show_all
3229
3230         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3231         nperrow_group = nil
3232         nperrows = []
3233         $allowed_N_values.each { |n|
3234             if nperrow_group
3235                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3236             else
3237                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3238             end
3239             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3240             if $default_N == n
3241                 rb.active = true
3242             end
3243             nperrows << { :widget => rb, :value => n }
3244         }
3245         nperrowradios.show_all
3246     }
3247     recreate_theme_config.call
3248
3249     theme_button.signal_connect('clicked') {
3250         if newtheme = theme_choose(theme_button.label)
3251             theme_button.label = newtheme
3252             recreate_theme_config.call
3253         end
3254     }
3255
3256     dialog.vbox.add(frame1)
3257     dialog.vbox.add(frame2)
3258     dialog.show_all
3259
3260     keepon = true
3261     ok = true
3262     while keepon
3263         dialog.run { |response|
3264             if response == Gtk::Dialog::RESPONSE_OK
3265                 srcdir = from_utf8_safe(src.text)
3266                 destdir = from_utf8_safe(dest.text)
3267                 confpath = from_utf8_safe(conf.text)
3268                 if src.text != '' && srcdir == ''
3269                     show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3270                     src.grab_focus
3271                 elsif !File.directory?(srcdir)
3272                     show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3273                     src.grab_focus
3274                 elsif dest.text != '' && destdir == ''
3275                     show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3276                     dest.grab_focus
3277                 elsif destdir != make_dest_filename(destdir)
3278                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3279                     dest.grab_focus
3280                 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3281                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
3282                     dest.grab_focus
3283                 elsif File.exists?(destdir) && !File.directory?(destdir)
3284                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3285                     dest.grab_focus
3286                 elsif conf.text == ''
3287                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3288                     conf.grab_focus
3289                 elsif conf.text != '' && confpath == ''
3290                     show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3291                     conf.grab_focus
3292                 elsif File.directory?(confpath)
3293                     show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3294                     conf.grab_focus
3295                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3296                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3297                 else
3298                     system("mkdir '#{destdir}'")
3299                     if !File.directory?(destdir)
3300                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3301                         dest.grab_focus
3302                     else
3303                         keepon = false
3304                     end
3305                 end
3306             else
3307                 keepon = ok = false
3308             end
3309         }
3310     end
3311     if ok
3312         srcdir = from_utf8(src.text)
3313         destdir = from_utf8(dest.text)
3314         configskel = File.expand_path(from_utf8(conf.text))
3315         theme = theme_button.label
3316         #- some sort of automatic theme preference
3317         $config['default-theme'] = theme
3318         sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3319         nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3320         nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3321         opt432 = optimize432.active?
3322         madewith = madewithentry.text.gsub('\'', '&#39;')  #- because the parameters to booh-backend are between apostrophes
3323         indexlink = indexlinkentry.text.gsub('\'', '&#39;')
3324     end
3325     if src_nb_thread
3326         Thread.kill(src_nb_thread)
3327         gtk_thread_flush  #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3328     end
3329     dialog.destroy
3330     Gtk.timeout_remove(timeout_src_nb)
3331
3332     if ok
3333         call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3334                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3335                      (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3336                      (multilanguages_value ? "--multi-languages #{multilanguages_value}" : '') +
3337                      "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3338                      utf8(_("Please wait while scanning source directory...")),
3339                      'full scan',
3340                      { :closure_after => proc { open_file_user(configskel) } })
3341     end
3342 end
3343
3344 def properties
3345     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3346                              $main_window,
3347                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3348                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3349                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3350     
3351     source = $xmldoc.root.attributes['source']
3352     dest = $xmldoc.root.attributes['destination']
3353     theme = $xmldoc.root.attributes['theme']
3354     opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3355     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3356     nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3357     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3358     if limit_sizes
3359         limit_sizes = limit_sizes.split(/,/)
3360     end
3361     madewith = ($xmldoc.root.attributes['made-with'] || '').gsub('&#39;', '\'')
3362     indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub('&#39;', '\'')
3363     save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3364
3365     tooltips = Gtk::Tooltips.new
3366     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3367     tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3368                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3369     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3370                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3371     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3372                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3373     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3374                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3375     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3376                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3377     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3378                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3379
3380     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3381     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3382                                    pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3383     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3384                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3385     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3386     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)
3387     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3388                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3389     nperpage_model = Gtk::ListStore.new(String, String)
3390     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3391                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3392     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3393     nperpagecombo.set_attributes(crt, { :markup => 0 })
3394     iter = nperpage_model.append
3395     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3396     iter[1] = nil
3397     [ 12, 20, 30, 40, 50 ].each { |v|
3398         iter = nperpage_model.append
3399         iter[0] = iter[1] = v.to_s
3400         if nperpage && nperpage == v.to_s
3401             nperpagecombo.active_iter = iter
3402         end
3403     }
3404     if nperpagecombo.active_iter.nil?
3405         nperpagecombo.active = 0
3406     end
3407
3408     vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3409                                         pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3410     tooltips.set_tip(ml, utf8(_("When disabled, the web-album will be generated with navigation in your desktop language. When enabled, the web-album will be generated with navigation in all languages you select, but you have to publish your web-album on an Apache web-server for that feature to work.")), nil)
3411     ml_update = proc {
3412         if save_multilanguages_value
3413             ml_label.text = utf8(_("Multi-languages: enabled."))
3414         else
3415             ml_label.text = utf8(_("Multi-languages: disabled."))
3416         end
3417     }
3418     ml_update.call
3419     multilanguages.signal_connect('clicked') {
3420         retval = ask_multi_languages(save_multilanguages_value)
3421         if retval[0] 
3422             save_multilanguages_value = retval[1]
3423         end
3424         ml_update.call
3425     }
3426
3427     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3428                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3429     if indexlink
3430         indexlinkentry.text = indexlink
3431     end
3432     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)
3433     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3434                                    pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3435     if madewith
3436         madewithentry.text = madewith
3437     end
3438     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)
3439
3440     theme_sizes = []
3441     nperrows = []
3442     recreate_theme_config = proc {
3443         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3444         theme_sizes = []
3445         select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3446
3447         $images_size.each { |s|
3448             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3449             if limit_sizes
3450                 if limit_sizes.include?(s['name'])
3451                     cb.active = true
3452                 end
3453             else
3454                 if !s['optional']
3455                     cb.active = true
3456                 end
3457             end
3458             tooltips.set_tip(cb, utf8(s['description']), nil)
3459             theme_sizes << { :widget => cb, :value => s['name'] }
3460         }
3461         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3462         tooltips = Gtk::Tooltips.new
3463         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3464         if limit_sizes && limit_sizes.include?('original')
3465             cb.active = true
3466         end
3467         theme_sizes << { :widget => cb, :value => 'original' }
3468         sizes.show_all
3469
3470         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3471         nperrow_group = nil
3472         nperrows = []
3473         $allowed_N_values.each { |n|
3474             if nperrow_group
3475                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3476             else
3477                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3478             end
3479             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3480             nperrowradios.add(Gtk::Label.new('  '))
3481             if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3482                 rb.active = true
3483             end
3484             nperrows << { :widget => rb, :value => n.to_s }
3485         }
3486         nperrowradios.show_all
3487     }
3488     recreate_theme_config.call
3489
3490     theme_button.signal_connect('clicked') {
3491         if newtheme = theme_choose(theme_button.label)
3492             limit_sizes = nil
3493             nperrow = nil
3494             theme_button.label = newtheme
3495             recreate_theme_config.call
3496         end
3497     }
3498
3499     dialog.vbox.add(frame1)
3500     dialog.vbox.add(frame2)
3501     dialog.show_all
3502
3503     keepon = true
3504     ok = true
3505     while keepon
3506         dialog.run { |response|
3507             if response == Gtk::Dialog::RESPONSE_OK
3508                 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3509   &nb