support widescreen 1680x1050
[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(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2207             end
2208         else
2209             exec(cmd)
2210         end
2211     }
2212     button.signal_connect('clicked') {
2213         Process.kill('SIGTERM', pid)
2214     }
2215 end
2216
2217 def save_changes(*forced)
2218     if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2219         return
2220     end
2221
2222     $xmldir.delete_attribute('already-generated')
2223
2224     propagate_children = proc { |xmldir|
2225         if xmldir.attributes['subdirs-caption']
2226             xmldir.delete_attribute('already-generated')
2227         end
2228         xmldir.elements.each('dir') { |element|
2229             propagate_children.call(element)
2230         }
2231     }
2232
2233     if $xmldir.child_byname_notattr('dir', 'deleted')
2234         new_title = $subalbums_title.buffer.text
2235         if new_title != $xmldir.attributes['subdirs-caption']
2236             parent = $xmldir.parent
2237             if parent.name == 'dir'
2238                 parent.delete_attribute('already-generated')
2239             end
2240             propagate_children.call($xmldir)
2241         end
2242         $xmldir.add_attribute('subdirs-caption', new_title)
2243         $xmldir.elements.each('dir') { |element|
2244             if !element.attributes['deleted']
2245                 path = element.attributes['path']
2246                 newtext = $subalbums_edits[path][:editzone].buffer.text
2247                 if element.attributes['subdirs-caption']
2248                     if element.attributes['subdirs-caption'] != newtext
2249                         propagate_children.call(element)
2250                     end
2251                     element.add_attribute('subdirs-caption',     newtext)
2252                     element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2253                 else
2254                     if element.attributes['thumbnails-caption'] != newtext
2255                         element.delete_attribute('already-generated')
2256                     end
2257                     element.add_attribute('thumbnails-caption',     newtext)
2258                     element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2259                 end
2260             end
2261         }
2262     end
2263
2264     if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2265         if $xmldir.attributes['thumbnails-caption']
2266             path = $xmldir.attributes['path']
2267             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2268         end
2269     elsif $xmldir.attributes['thumbnails-caption']
2270         $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2271     end
2272
2273     if $xmldir.attributes['thumbnails-caption']
2274         if edit = $subalbums_edits[$xmldir.attributes['path']]
2275             $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2276         end
2277     end
2278
2279     #- remove and reinsert elements to reflect new ordering
2280     saves = {}
2281     cpt = 0
2282     $xmldir.elements.each { |element|
2283         if element.name == 'image' || element.name == 'video'
2284             saves[element.attributes['filename']] = element.remove
2285             cpt += 1
2286         end
2287     }
2288     $autotable.current_order.each { |path|
2289         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2290         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2291         saves.delete(path)
2292     }
2293     saves.each_key { |path|
2294         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2295         chld.add_attribute('deleted', 'true')
2296     }
2297 end
2298
2299 def sort_by_exif_date
2300     $modified = true
2301     save_changes
2302     current_order = []
2303     rexml_thread_protect {
2304         $xmldir.elements.each { |element|
2305             if element.name == 'image' || element.name == 'video'
2306                 current_order << element.attributes['filename']
2307             end
2308         }
2309     }
2310
2311     #- look for EXIF dates
2312     dates = {}
2313
2314     if current_order.size > 20
2315         w = create_window
2316         w.set_transient_for($main_window)
2317         w.modal = true
2318         vb = Gtk::VBox.new(false, 5).set_border_width(5)
2319         vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2320         vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2321         bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2322         b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2323         vb.pack_end(bottom, false, false)
2324         w.add(vb)
2325         w.signal_connect('delete-event') { w.destroy }
2326         w.window_position = Gtk::Window::POS_CENTER
2327         w.show_all
2328
2329         aborted = false
2330         b.signal_connect('clicked') { aborted = true }
2331         i = 0
2332         current_order.each { |f|
2333             i += 1
2334             if entry2type(f) == 'image'
2335                 pb.text = f
2336                 pb.fraction = i.to_f / current_order.size
2337                 Gtk.main_iteration while Gtk.events_pending?
2338                 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2339                 if ! date_time.nil?
2340                     dates[f] = date_time
2341                 end
2342             end
2343             if aborted
2344                 break
2345             end
2346         }
2347         w.destroy
2348         if aborted
2349             return
2350         end
2351
2352     else
2353         current_order.each { |f|
2354             date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2355             if ! date_time.nil?
2356                 dates[f] = date_time
2357             end
2358         }
2359     end
2360
2361     saves = {}
2362     rexml_thread_protect {
2363         $xmldir.elements.each { |element|
2364             if element.name == 'image' || element.name == 'video'
2365                 saves[element.attributes['filename']] = element.remove
2366             end
2367         }
2368     }
2369
2370     neworder = smartsort(current_order, dates)
2371
2372     rexml_thread_protect {
2373         neworder.each { |f|
2374             $xmldir.add_element(saves[f].name, saves[f].attributes)
2375         }
2376     }
2377
2378     #- let the auto-table reflect new ordering
2379     change_dir
2380 end
2381
2382 def remove_all_captions
2383     $modified = true
2384     texts = {}
2385     $autotable.current_order.each { |path|
2386         texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2387         $name2widgets[File.basename(path)][:textview].buffer.text = ''
2388     }
2389     save_undo(_("remove all captions"),
2390               proc { |texts|
2391                   texts.each_key { |key|
2392                       $name2widgets[key][:textview].buffer.text = texts[key]
2393                   }
2394                   $notebook.set_page(1)
2395                   proc {
2396                       texts.each_key { |key|
2397                           $name2widgets[key][:textview].buffer.text = ''
2398                       }
2399                       $notebook.set_page(1)
2400                   }
2401               }, texts)
2402 end
2403
2404 def change_dir
2405     $selected_elements.each_key { |path|
2406         $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2407     }
2408     $autotable.clear
2409     $vbox2widgets = {}
2410     $name2widgets = {}
2411     $name2closures = {}
2412     $selected_elements = {}
2413     $cuts = []
2414     $multiple_dnd = []
2415     UndoHandler.cleanup
2416     $undo_tb.sensitive = $undo_mb.sensitive = false
2417     $redo_tb.sensitive = $redo_mb.sensitive = false
2418
2419     if !$current_path
2420         return
2421     end
2422
2423     $subalbums_vb.children.each { |chld|
2424         $subalbums_vb.remove(chld)
2425     }
2426     $subalbums = Gtk::Table.new(0, 0, true)
2427     current_y_sub_albums = 0
2428
2429     $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2430     $subalbums_edits = {}
2431     subalbums_counter = 0
2432     subalbums_edits_bypos = {}
2433
2434     add_subalbum = proc { |xmldir, counter|
2435         $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2436         subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2437         if xmldir == $xmldir
2438             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2439             captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2440             caption = xmldir.attributes['thumbnails-caption']
2441             infotype = 'thumbnails'
2442         else
2443             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2444             captionfile, caption = find_subalbum_caption_info(xmldir)
2445             infotype = find_subalbum_info_type(xmldir)
2446         end
2447         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2448         hbox = Gtk::HBox.new
2449         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2450         f = Gtk::Frame.new
2451         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2452
2453         img = nil
2454         my_gen_real_thumbnail = proc {
2455             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2456         }
2457
2458         if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2459             f.add(img = Gtk::Image.new)
2460             my_gen_real_thumbnail.call
2461         else
2462             f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2463         end
2464         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2465         $subalbums.attach(hbox,
2466                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2467
2468         frame, textview = create_editzone($subalbums_sw, 0, img)
2469         textview.buffer.text = caption
2470         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2471                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2472
2473         change_image = proc {
2474             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2475                                             nil,
2476                                             Gtk::FileChooser::ACTION_OPEN,
2477                                             nil,
2478                                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2479             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2480             fc.transient_for = $main_window
2481             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))
2482             f.add(preview_img = Gtk::Image.new)
2483             preview.show_all
2484             fc.signal_connect('update-preview') { |w|
2485                 if fc.preview_filename
2486                     if entry2type(fc.preview_filename) == 'video'
2487                         image_path = nil
2488                         tmpdir = nil
2489                         begin
2490                             tmpdir = gen_video_thumbnail(fc.preview_filename, false, 0)
2491                             if tmpdir.nil?
2492                                 fc.preview_widget_active = false
2493                             else
2494                                 tmpimage = "#{tmpdir}/00000001.jpg"
2495                                 begin
2496                                     preview_img.pixbuf = Gdk::Pixbuf.new(tmpimage, 240, 180)
2497                                     fc.preview_widget_active = true
2498                                 rescue Gdk::PixbufError
2499                                     fc.preview_widget_active = false
2500                                 ensure
2501                                     File.delete(tmpimage)
2502                                     Dir.rmdir(tmpdir)
2503                                 end
2504                             end
2505                         end
2506                     else
2507                         begin
2508                             preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2509                             fc.preview_widget_active = true
2510                         rescue Gdk::PixbufError
2511                             fc.preview_widget_active = false
2512                         end
2513                     end
2514                 end
2515             }
2516             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2517                 $modified = true
2518                 old_file = captionfile
2519                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2520                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2521                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2522                 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2523
2524                 new_file = fc.filename
2525                 msg 3, "new captionfile is: #{fc.filename}"
2526                 perform_changefile = proc {
2527                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2528                     $modified_pixbufs.delete(thumbnail_file)
2529                     xmldir.delete_attribute("#{infotype}-rotate")
2530                     xmldir.delete_attribute("#{infotype}-color-swap")
2531                     xmldir.delete_attribute("#{infotype}-enhance")
2532                     xmldir.delete_attribute("#{infotype}-seektime")
2533                     my_gen_real_thumbnail.call
2534                 }
2535                 perform_changefile.call
2536
2537                 save_undo(_("change caption file for sub-album"),
2538                           proc {
2539                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2540                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2541                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2542                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2543                               xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2544                               my_gen_real_thumbnail.call
2545                               $notebook.set_page(0)
2546                               proc {
2547                                   perform_changefile.call
2548                                   $notebook.set_page(0)
2549                               }
2550                           })
2551             end
2552             fc.destroy
2553         }
2554
2555         refresh = proc {
2556             if File.exists?(thumbnail_file)
2557                 File.delete(thumbnail_file)
2558             end
2559             my_gen_real_thumbnail.call
2560         }
2561
2562         rotate_and_cleanup = proc { |angle|
2563             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2564             if File.exists?(thumbnail_file)
2565                 File.delete(thumbnail_file)
2566             end
2567         }
2568
2569         move = proc { |direction|
2570             $modified = true
2571
2572             save_changes('forced')
2573             oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2574             if direction == 'up'
2575                 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2576                 subalbums_edits_bypos[oldpos - 1][:position] += 1
2577             end
2578             if direction == 'down'
2579                 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2580                 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2581             end
2582             if direction == 'top'
2583                 for i in 1 .. oldpos - 1
2584                     subalbums_edits_bypos[i][:position] += 1
2585                 end
2586                 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2587             end
2588             if direction == 'bottom'
2589                 for i in oldpos + 1 .. subalbums_counter
2590                     subalbums_edits_bypos[i][:position] -= 1
2591                 end
2592                 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2593             end
2594
2595             elems = []
2596             $xmldir.elements.each('dir') { |element|
2597                 if (!element.attributes['deleted'])
2598                     elems << [ element.attributes['path'], element.remove ]
2599                 end
2600             }
2601             elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2602                   each { |e| $xmldir.add_element(e[1]) }
2603             #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2604             $xmldir.elements.each('descendant::dir') { |elem|
2605                 elem.delete_attribute('already-generated')
2606             }
2607
2608             sel = $albums_tv.selection.selected_rows
2609             change_dir
2610             populate_subalbums_treeview(false)
2611             $albums_tv.selection.select_path(sel[0])
2612         }
2613
2614         color_swap_and_cleanup = proc {
2615             perform_color_swap_and_cleanup = proc {
2616                 color_swap(xmldir, "#{infotype}-")
2617                 my_gen_real_thumbnail.call
2618             }
2619             perform_color_swap_and_cleanup.call
2620
2621             save_undo(_("color swap"),
2622                       proc {
2623                           perform_color_swap_and_cleanup.call
2624                           $notebook.set_page(0)
2625                           proc {
2626                               perform_color_swap_and_cleanup.call
2627                               $notebook.set_page(0)
2628                           }
2629                       })
2630         }
2631
2632         change_seektime_and_cleanup = proc {
2633             if values = ask_new_seektime(xmldir, "#{infotype}-")
2634                 perform_change_seektime_and_cleanup = proc { |val|
2635                     change_seektime(xmldir, "#{infotype}-", val)
2636                     my_gen_real_thumbnail.call
2637                 }
2638                 perform_change_seektime_and_cleanup.call(values[:new])
2639
2640                 save_undo(_("specify seektime"),
2641                           proc {
2642                               perform_change_seektime_and_cleanup.call(values[:old])
2643                               $notebook.set_page(0)
2644                               proc {
2645                                   perform_change_seektime_and_cleanup.call(values[:new])
2646                                   $notebook.set_page(0)
2647                               }
2648                           })
2649             end
2650         }
2651
2652         whitebalance_and_cleanup = proc {
2653             if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2654                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2655                 perform_change_whitebalance_and_cleanup = proc { |val|
2656                     change_whitebalance(xmldir, "#{infotype}-", val)
2657                     recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2658                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2659                     if File.exists?(thumbnail_file)
2660                         File.delete(thumbnail_file)
2661                     end
2662                 }
2663                 perform_change_whitebalance_and_cleanup.call(values[:new])
2664                 
2665                 save_undo(_("fix white balance"),
2666                           proc {
2667                               perform_change_whitebalance_and_cleanup.call(values[:old])
2668                               $notebook.set_page(0)
2669                               proc {
2670                                   perform_change_whitebalance_and_cleanup.call(values[:new])
2671                                   $notebook.set_page(0)
2672                               }
2673                           })
2674             end
2675         }
2676
2677         gammacorrect_and_cleanup = proc {
2678             if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2679                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2680                 perform_change_gammacorrect_and_cleanup = proc { |val|
2681                     change_gammacorrect(xmldir, "#{infotype}-", val)
2682                     recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2683                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2684                     if File.exists?(thumbnail_file)
2685                         File.delete(thumbnail_file)
2686                     end
2687                 }
2688                 perform_change_gammacorrect_and_cleanup.call(values[:new])
2689                 
2690                 save_undo(_("gamma correction"),
2691                           proc {
2692                               perform_change_gammacorrect_and_cleanup.call(values[:old])
2693                               $notebook.set_page(0)
2694                               proc {
2695                                   perform_change_gammacorrect_and_cleanup.call(values[:new])
2696                                   $notebook.set_page(0)
2697                               }
2698                           })
2699             end
2700         }
2701
2702         enhance_and_cleanup = proc {
2703             perform_enhance_and_cleanup = proc {
2704                 enhance(xmldir, "#{infotype}-")
2705                 my_gen_real_thumbnail.call
2706             }
2707             
2708             perform_enhance_and_cleanup.call
2709             
2710             save_undo(_("enhance"),
2711                       proc {
2712                           perform_enhance_and_cleanup.call
2713                           $notebook.set_page(0)
2714                           proc {
2715                               perform_enhance_and_cleanup.call
2716                               $notebook.set_page(0)
2717                           }
2718                       })
2719         }
2720
2721         evtbox.signal_connect('button-press-event') { |w, event|
2722             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2723                 if $r90.active?
2724                     rotate_and_cleanup.call(90)
2725                 elsif $r270.active?
2726                     rotate_and_cleanup.call(-90)
2727                 elsif $enhance.active?
2728                     enhance_and_cleanup.call
2729                 end
2730             end
2731             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2732                 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2733                                      { :forbid_left => true, :forbid_right => true,
2734                                        :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2735                                        :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2736                                      { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2737                                        :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2738                                        :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2739             end
2740             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2741                 change_image.call
2742                 true   #- handled
2743             end
2744         }
2745         evtbox.signal_connect('button-press-event') { |w, event|
2746             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2747             false
2748         }
2749
2750         evtbox.signal_connect('button-release-event') { |w, event|
2751             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2752                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2753                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2754                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2755                     msg 3, "gesture rotate: #{angle}"
2756                     rotate_and_cleanup.call(angle)
2757                 end
2758             end
2759             $gesture_press = nil
2760         }
2761                 
2762         $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2763         $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2764         current_y_sub_albums += 1
2765     }
2766
2767     if $xmldir.child_byname_notattr('dir', 'deleted')
2768         #- title edition
2769         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2770         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] || ''
2771         $subalbums_title.set_justification(Gtk::Justification::CENTER)
2772         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2773         #- this album image/caption
2774         if $xmldir.attributes['thumbnails-caption']
2775             add_subalbum.call($xmldir, 0)
2776         end
2777     end
2778     total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2779     $xmldir.elements.each { |element|
2780         if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2781             #- element (image or video) of this album
2782             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2783             msg 3, "dest_img: #{dest_img}"
2784             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2785             total[element.name] += 1
2786         end
2787         if element.name == 'dir' && !element.attributes['deleted']
2788             #- sub-album image/caption
2789             add_subalbum.call(element, subalbums_counter += 1)
2790             total[element.name] += 1
2791         end
2792     }
2793     $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2794                                                                                 total['image'], total['video'], total['dir'] ]))
2795     $subalbums_vb.add($subalbums)
2796     $subalbums_vb.show_all
2797
2798     if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2799         $notebook.get_tab_label($autotable_sw).sensitive = false
2800         $notebook.set_page(0)
2801         $thumbnails_title.buffer.text = ''
2802     else
2803         $notebook.get_tab_label($autotable_sw).sensitive = true
2804         $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2805     end
2806
2807     if !$xmldir.child_byname_notattr('dir', 'deleted')
2808         $notebook.get_tab_label($subalbums_sw).sensitive = false
2809         $notebook.set_page(1)
2810     else
2811         $notebook.get_tab_label($subalbums_sw).sensitive = true
2812     end
2813 end
2814
2815 def pixbuf_or_nil(filename)
2816     begin
2817         return Gdk::Pixbuf.new(filename)
2818     rescue
2819         return nil
2820     end
2821 end
2822
2823 def theme_choose(current)
2824     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2825                              $main_window,
2826                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2827                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2828                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2829
2830     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2831     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2832     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2833     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2834     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2835     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2836     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2837     treeview.signal_connect('button-press-event') { |w, event|
2838         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2839             dialog.response(Gtk::Dialog::RESPONSE_OK)
2840         end
2841     }
2842
2843     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2844
2845     ([ $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|
2846         dir.chomp!
2847         iter = model.append
2848         iter[0] = File.basename(dir)
2849         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2850         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2851         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2852         if File.basename(dir) == current
2853             treeview.selection.select_iter(iter)
2854         end
2855     }
2856     dialog.set_default_size(-1, 500)
2857     dialog.vbox.show_all
2858
2859     dialog.run { |response|
2860         iter = treeview.selection.selected
2861         dialog.destroy
2862         if response == Gtk::Dialog::RESPONSE_OK && iter
2863             return model.get_value(iter, 0)
2864         end
2865     }
2866     return nil
2867 end
2868
2869 def show_password_protections
2870     examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2871         child_iter = $albums_iters[xmldir.attributes['path']]
2872         if xmldir.attributes['password-protect']
2873             child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2874             already_protected = true
2875         elsif already_protected
2876             pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2877             if pix
2878                 pix = pix.saturate_and_pixelate(1, true)
2879             end
2880             child_iter[2] = pix
2881         else
2882             child_iter[2] = nil
2883         end
2884         xmldir.elements.each('dir') { |elem|
2885             if !elem.attributes['deleted']
2886                 examine_dir_elem.call(child_iter, elem, already_protected)
2887             end
2888         }
2889     }
2890     examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2891 end
2892
2893 def populate_subalbums_treeview(select_first)
2894     $albums_ts.clear
2895     $autotable.clear
2896     $albums_iters = {}
2897     $subalbums_vb.children.each { |chld|
2898         $subalbums_vb.remove(chld)
2899     }
2900
2901     source = $xmldoc.root.attributes['source']
2902     msg 3, "source: #{source}"
2903
2904     xmldir = $xmldoc.elements['//dir']
2905     if !xmldir || xmldir.attributes['path'] != source
2906         msg 1, _("Corrupted booh file...")
2907         return
2908     end
2909
2910     append_dir_elem = proc { |parent_iter, xmldir|
2911         child_iter = $albums_ts.append(parent_iter)
2912         child_iter[0] = File.basename(xmldir.attributes['path'])
2913         child_iter[1] = xmldir.attributes['path']
2914         $albums_iters[xmldir.attributes['path']] = child_iter
2915         msg 3, "puttin location: #{xmldir.attributes['path']}"
2916         xmldir.elements.each('dir') { |elem|
2917             if !elem.attributes['deleted']
2918                 append_dir_elem.call(child_iter, elem)
2919             end
2920         }
2921     }
2922     append_dir_elem.call(nil, xmldir)
2923     show_password_protections
2924
2925     $albums_tv.expand_all
2926     if select_first
2927         $albums_tv.selection.select_iter($albums_ts.iter_first)
2928     end
2929 end
2930
2931 def select_current_theme
2932     select_theme($xmldoc.root.attributes['theme'],
2933                  $xmldoc.root.attributes['limit-sizes'],
2934                  !$xmldoc.root.attributes['optimize-for-32'].nil?,
2935                  $xmldoc.root.attributes['thumbnails-per-row'])
2936 end
2937
2938 def open_file(filename)
2939
2940     $filename = nil
2941     $modified = false
2942     $current_path = nil   #- invalidate
2943     $modified_pixbufs = {}
2944     $albums_ts.clear
2945     $autotable.clear
2946     $subalbums_vb.children.each { |chld|
2947         $subalbums_vb.remove(chld)
2948     }
2949
2950     if !File.exists?(filename)
2951         return utf8(_("File not found."))
2952     end
2953
2954     begin
2955         $xmldoc = REXML::Document.new(File.new(filename))
2956     rescue Exception
2957         $xmldoc = nil
2958     end
2959
2960     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2961         if entry2type(filename).nil?
2962             return utf8(_("Not a booh file!"))
2963         else
2964             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."))
2965         end
2966     end
2967
2968     if !source = $xmldoc.root.attributes['source']
2969         return utf8(_("Corrupted booh file..."))
2970     end
2971
2972     if !dest = $xmldoc.root.attributes['destination']
2973         return utf8(_("Corrupted booh file..."))
2974     end
2975
2976     if !theme = $xmldoc.root.attributes['theme']
2977         return utf8(_("Corrupted booh file..."))
2978     end
2979
2980     if $xmldoc.root.attributes['version'] < $VERSION
2981         msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2982         mark_document_as_dirty
2983         if $xmldoc.root.attributes['version'] < '0.8.4'
2984             msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2985             `find '#{source}' -type d -follow`.split("\n").sort.collect { |v| v.chomp }.each { |dir|
2986                 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2987                 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2988                 if old_dest_dir != new_dest_dir
2989                     sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2990                 end
2991                 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2992                     xmldir.elements.each { |element|
2993                         if %w(image video).include?(element.name) && !element.attributes['deleted']
2994                             old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2995                             new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2996                             Dir[old_name + '*'].each { |file|
2997                                 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2998                                 file != new_file and sys("mv '#{file}' '#{new_file}'")
2999                             }
3000                         end
3001                         if element.name == 'dir' && !element.attributes['deleted']
3002                             old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
3003                             new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
3004                             old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
3005                         end
3006                     }
3007                 else
3008                     msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
3009                 end
3010             }
3011         end
3012         $xmldoc.root.add_attribute('version', $VERSION)
3013     end
3014
3015     select_current_theme
3016
3017     $filename = filename
3018     set_mainwindow_title(nil)
3019     $default_size['thumbnails'] =~ /(.*)x(.*)/
3020     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3021     $albums_thumbnail_size =~ /(.*)x(.*)/
3022     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3023
3024     populate_subalbums_treeview(true)
3025
3026     $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
3027     return nil
3028 end
3029
3030 def open_file_user(filename)
3031     result = open_file(filename)
3032     if !result
3033         $config['last-opens'] ||= []
3034         if $config['last-opens'][-1] != utf8(filename)
3035             $config['last-opens'] << utf8(filename)
3036         end
3037         $orig_filename = $filename
3038         $main_window.title = 'booh - ' + File.basename($orig_filename)
3039         tmp = Tempfile.new("boohtemp")
3040         $filename = tmp.path
3041         tmp.close!
3042         #- for security
3043         ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
3044         ios.close
3045         $tempfiles << $filename << "#{$filename}.backup"
3046     else
3047         $orig_filename = nil
3048     end
3049     return result
3050 end
3051
3052 def open_file_popup
3053     if !ask_save_modifications(utf8(_("Save this album?")),
3054                                utf8(_("Do you want to save the changes to this album?")),
3055                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3056         return
3057     end
3058     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
3059                                     nil,
3060                                     Gtk::FileChooser::ACTION_OPEN,
3061                                     nil,
3062                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3063     fc.add_shortcut_folder(File.expand_path("~/.booh"))
3064     fc.set_current_folder(File.expand_path("~/.booh"))
3065     fc.transient_for = $main_window
3066     fc.preview_widget = previewlabel = Gtk::Label.new.show
3067     fc.signal_connect('update-preview') { |w|
3068         if fc.preview_filename
3069             begin
3070                 push_mousecursor_wait(fc)
3071                 xmldoc = REXML::Document.new(File.new(fc.preview_filename))
3072                 subalbums = 0
3073                 images = 0
3074                 videos = 0
3075                 xmldoc.elements.each('//*') { |elem|
3076                     if elem.name == 'dir'
3077                         subalbums += 1
3078                     elsif elem.name == 'image'
3079                         images += 1
3080                     elsif elem.name == 'video'
3081                         videos += 1
3082                     end
3083                 }
3084             rescue Exception
3085             ensure
3086                 pop_mousecursor(fc)
3087             end
3088             if !xmldoc || !xmldoc.root || xmldoc.root.name != 'booh'
3089                 fc.preview_widget_active = false
3090             else
3091                 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") %
3092                                            [ xmldoc.root.attributes['source'], xmldoc.root.attributes['destination'], subalbums, images, videos ])
3093                 fc.preview_widget_active = true
3094             end
3095         end
3096     }
3097     ok = false
3098     while !ok
3099         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3100             push_mousecursor_wait(fc)
3101             msg = open_file_user(fc.filename)
3102             pop_mousecursor(fc)
3103             if msg
3104                 show_popup(fc, msg)
3105                 ok = false
3106             else
3107                 ok = true
3108             end
3109         else
3110             ok = true
3111         end
3112     end
3113     fc.destroy
3114 end
3115
3116 def additional_booh_options
3117     options = ''
3118     if $config['mproc']
3119         options += "--mproc #{$config['mproc'].to_i} "
3120     end
3121     options += "--comments-format '#{$config['comments-format']}' "
3122     if $config['transcode-videos']
3123         options += "--transcode-videos '#{$config['transcode-videos']}' "
3124     end
3125     if $config['use-flv'] == 'true'
3126         options += "--flv-generator '#{$config['flv-generator']}' "
3127     end
3128     return options
3129 end
3130
3131 def ask_multi_languages(value)
3132     if ! value.nil?
3133         spl = value.split(',')
3134         value = [ spl[0..-2], spl[-1] ]
3135     end
3136
3137     dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
3138                              $main_window,
3139                              Gtk::Dialog::MODAL,
3140                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3141                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3142
3143     lbl = Gtk::Label.new
3144     lbl.markup = utf8(
3145 _("You can choose to activate <b>multi-languages</b> support for this web-album
3146 (it will work only if you publish your web-album on an Apache web-server). This will
3147 use the MultiViews feature of Apache; the pages will be served according to the
3148 value of the Accept-Language HTTP header sent by the web browsers, so that people
3149 with different languages preferences will be able to browse your web-album with
3150 navigation in their language (if language is available).
3151 "))
3152
3153     dialog.vbox.add(lbl)
3154     dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3155                                                                          add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3156                                                                                                      add(languages = Gtk::Button.new))))
3157
3158     pick_languages = proc {
3159         dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3160                                   $main_window,
3161                                   Gtk::Dialog::MODAL,
3162                                   [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3163                                   [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3164
3165         dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3166         hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3167         cbs = []
3168         SUPPORTED_LANGUAGES.each { |lang|
3169             hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3170             if ! value.nil? && value[0].include?(lang)
3171                 cb.active = true
3172             end
3173             cbs << [ lang, cb ]
3174         }
3175
3176         dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3177         hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3178         fallback_language = nil
3179         hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3180         fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3181         if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3182             fbl_rb.active = true
3183             fallback_language = SUPPORTED_LANGUAGES[0]
3184         end
3185         SUPPORTED_LANGUAGES[1..-1].each { |lang|
3186             hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3187             rb.signal_connect('clicked') { fallback_language = lang }
3188             if ! value.nil? && value[1] == lang
3189                 rb.active = true
3190             end
3191         }
3192
3193         dialog2.window_position = Gtk::Window::POS_MOUSE
3194         dialog2.show_all
3195
3196         resp = nil
3197         dialog2.run { |response|
3198             resp = response
3199             if resp == Gtk::Dialog::RESPONSE_OK
3200                 value = []
3201                 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3202                 value[1] = fallback_language
3203                 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3204             end
3205             dialog2.destroy
3206         }
3207         resp
3208     }
3209
3210     languages.signal_connect('clicked') {
3211         pick_languages.call
3212     }
3213     dialog.window_position = Gtk::Window::POS_MOUSE
3214     if value.nil?
3215         rb_no.active = true
3216     else
3217         rb_yes.active = true
3218         languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3219     end
3220     rb_no.signal_connect('clicked') {
3221         if rb_no.active?
3222             languages.hide
3223         else
3224             if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3225                 rb_no.activate
3226             else
3227                 languages.show
3228             end
3229         end
3230     }
3231     oldval = value
3232     dialog.show_all
3233     if rb_no.active?
3234         languages.hide
3235     end
3236
3237     dialog.run { |response|
3238         if rb_no.active?
3239             value = nil
3240         end
3241         dialog.destroy
3242         if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3243             if value.nil?
3244                 return [ true, nil ]
3245             else
3246                 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3247             end
3248         else
3249             return [ false ]
3250         end
3251     }
3252 end
3253
3254 def new_album
3255     if !ask_save_modifications(utf8(_("Save this album?")),
3256                                utf8(_("Do you want to save the changes to this album?")),
3257                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3258         return
3259     end
3260     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3261                              $main_window,
3262                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3263                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3264                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3265     
3266     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3267     tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3268                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3269     tbl.attach(src = Gtk::Entry.new,
3270                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3271     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3272                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3273     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3274                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3275     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3276                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3277     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3278                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3279     tbl.attach(dest = Gtk::Entry.new,
3280                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3281     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3282                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3283     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3284                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3285     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3286                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3287     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3288                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3289
3290     tooltips = Gtk::Tooltips.new
3291     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3292     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3293                          pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3294     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3295                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3296     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3297     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)
3298     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3299                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3300     nperpage_model = Gtk::ListStore.new(String, String)
3301     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3302                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3303     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3304     nperpagecombo.set_attributes(crt, { :markup => 0 })
3305     iter = nperpage_model.append
3306     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3307     iter[1] = nil
3308     [ 12, 20, 30, 40, 50 ].each { |v|
3309         iter = nperpage_model.append
3310         iter[0] = iter[1] = v.to_s
3311     }
3312     nperpagecombo.active = 0
3313
3314     multilanguages_value = nil
3315     vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3316                                         pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3317     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)
3318     multilanguages.signal_connect('clicked') {
3319         retval = ask_multi_languages(multilanguages_value)
3320         if retval[0] 
3321             multilanguages_value = retval[1]
3322         end
3323         if multilanguages_value
3324             ml_label.text = utf8(_("Multi-languages: enabled."))
3325         else
3326             ml_label.text = utf8(_("Multi-languages: disabled."))
3327         end
3328     }
3329     if $config['default-multi-languages']
3330         multilanguages_value = $config['default-multi-languages']
3331         ml_label.text = utf8(_("Multi-languages: enabled."))
3332     else
3333         ml_label.text = utf8(_("Multi-languages: disabled."))
3334     end
3335
3336     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3337                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3338     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)
3339     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3340                                    pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3341     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)
3342     vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active($config['default-addthis'].to_b))
3343     vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active($config['default-quotehtml'].to_b))
3344     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)
3345
3346     src_nb_calculated_for = ''
3347     src_nb_process = nil
3348     process_src_nb = proc {
3349         if src.text != src_nb_calculated_for
3350             src_nb_calculated_for = src.text
3351             if src_nb_process
3352                 begin
3353                     Process.kill(9, src_nb_process)
3354                 rescue Errno::ESRCH
3355                     #- process doesn't exist anymore - race condition
3356                 end
3357             end
3358             if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3359                 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3360             else
3361                 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3362                     if File.readable?(from_utf8_safe(src_nb_calculated_for))
3363                         rd, wr = IO.pipe
3364                         if src_nb_process
3365                             while src_nb_process
3366                                 msg 3, "sleeping for completion of previous process"
3367                                 sleep 0.05
3368                             end
3369                             gtk_thread_flush  #- flush to avoid race condition in src_nb markup update
3370                         end
3371                         src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
3372                         total = { 'image' => 0, 'video' => 0, nil => 0 }
3373                         if src_nb_process = fork
3374                             msg 3, "spawned #{src_nb_process} for #{src_nb_calculated_for}"
3375                             #- parent
3376                             wr.close
3377                             Thread.new {
3378                                 rd.readlines.each { |dir|
3379                                     if File.basename(dir) =~ /^\./
3380                                         next
3381                                     else
3382                                         begin
3383                                             Dir.entries(dir.chomp).each { |file|
3384                                                 total[entry2type(file)] += 1
3385                                             }
3386                                         rescue Errno::EACCES, Errno::ENOENT
3387                                         end
3388                                     end
3389                                 }
3390                                 rd.close
3391                                 msg 3, "ripping #{src_nb_process}"
3392                                 dummy, exitstatus = Process.waitpid2(src_nb_process)
3393                                 if exitstatus == 0
3394                                     gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3395                                 end
3396                                 src_nb_process = nil
3397                             }
3398                             
3399                         else
3400                             #- child
3401                             rd.close
3402                             wr.write(`find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`)
3403                             Process.exit!(0)  #- _exit
3404                         end                       
3405                     else
3406                         src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3407                     end
3408                 else
3409                     src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3410                 end
3411             end
3412         end
3413         true
3414     }
3415     timeout_src_nb = Gtk.timeout_add(100) {
3416         process_src_nb.call
3417     }
3418
3419     src_browse.signal_connect('clicked') {
3420         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3421                                         nil,
3422                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
3423                                         nil,
3424                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3425         fc.transient_for = $main_window
3426         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3427             src.text = utf8(fc.filename)
3428             process_src_nb.call
3429             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3430         end
3431         fc.destroy
3432     }
3433
3434     dest_browse.signal_connect('clicked') {
3435         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3436                                         nil,
3437                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
3438                                         nil,
3439                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3440         fc.transient_for = $main_window
3441         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3442             dest.text = utf8(fc.filename)
3443         end
3444         fc.destroy
3445     }
3446
3447     conf_browse.signal_connect('clicked') {
3448         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3449                                         nil,
3450                                         Gtk::FileChooser::ACTION_SAVE,
3451                                         nil,
3452                                         [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3453         fc.transient_for = $main_window
3454         fc.add_shortcut_folder(File.expand_path("~/.booh"))
3455         fc.set_current_folder(File.expand_path("~/.booh"))
3456         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3457             conf.text = utf8(fc.filename)
3458         end
3459         fc.destroy
3460     }
3461
3462     theme_sizes = []
3463     nperrows = []
3464     recreate_theme_config = proc {
3465         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3466         theme_sizes = []
3467         select_theme(theme_button.label, 'all', optimize432.active?, nil)
3468         $images_size.each { |s|
3469             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3470             if !s['optional']
3471                 cb.active = true
3472             end
3473             tooltips.set_tip(cb, utf8(s['description']), nil)
3474             theme_sizes << { :widget => cb, :value => s['name'] }
3475         }
3476         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3477         tooltips = Gtk::Tooltips.new
3478         tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3479         theme_sizes << { :widget => cb, :value => 'original' }
3480         sizes.show_all
3481
3482         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3483         nperrow_group = nil
3484         nperrows = []
3485         $allowed_N_values.each { |n|
3486             if nperrow_group
3487                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3488             else
3489                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3490             end
3491             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3492             if $default_N == n
3493                 rb.active = true
3494             end
3495             nperrows << { :widget => rb, :value => n }
3496         }
3497         nperrowradios.show_all
3498     }
3499     recreate_theme_config.call
3500
3501     theme_button.signal_connect('clicked') {
3502         if newtheme = theme_choose(theme_button.label)
3503             theme_button.label = newtheme
3504             recreate_theme_config.call
3505         end
3506     }
3507
3508     dialog.vbox.add(frame1)
3509     dialog.vbox.add(frame2)
3510     dialog.show_all
3511
3512     keepon = true