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