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