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