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