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