properly skip launching backend on "ok" in properties without value changes
[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-2009 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-2009 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'
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 %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][:topwidget]
2007         dialog.vbox.add(options[0][:topwidget])
2008     end
2009     if options[0] && options[0][:scrolled]
2010         sw = Gtk::ScrolledWindow.new(nil, nil)
2011         sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
2012         sw.add_with_viewport(lbl)
2013         dialog.vbox.add(sw)
2014         dialog.set_default_size(500, 600)
2015     else
2016         dialog.vbox.add(lbl)
2017         dialog.set_default_size(200, 120)
2018     end
2019     if options[0] && options[0][:okcancel]
2020         dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
2021     end
2022     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
2023
2024     if options[0] && options[0][:pos_centered]
2025         dialog.window_position = Gtk::Window::POS_CENTER
2026     else
2027         dialog.window_position = Gtk::Window::POS_MOUSE
2028     end
2029
2030     if options[0] && options[0][:linkurl]
2031         linkbut = Gtk::Button.new('')
2032         linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
2033         linkbut.signal_connect('clicked') {
2034             open_url(options[0][:linkurl])
2035             dialog.response(Gtk::Dialog::RESPONSE_OK)
2036             set_mousecursor_normal
2037         }
2038         linkbut.relief = Gtk::RELIEF_NONE
2039         linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
2040         linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
2041         dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
2042     end
2043
2044     dialog.show_all
2045
2046     if !options[0] || !options[0][:not_transient]
2047         dialog.transient_for = parent
2048         dialog.run { |response|
2049             dialog.destroy
2050             if options[0] && options[0][:okcancel]
2051                 return response == Gtk::Dialog::RESPONSE_OK
2052             end
2053         }
2054     else
2055         dialog.signal_connect('response') { dialog.destroy }
2056     end
2057 end
2058
2059 def set_mainwindow_title(progress)
2060     filename = $orig_filename || $filename
2061     if progress
2062         if filename
2063             $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
2064         else
2065             $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
2066         end
2067     else
2068         if filename
2069             $main_window.title = 'booh - ' + File.basename(filename)
2070         else
2071             $main_window.title = 'booh'
2072         end
2073     end
2074 end
2075
2076 def backend_wait_message(parent, msg, infopipe_path, mode)
2077     w = create_window
2078     w.set_transient_for(parent)
2079     w.modal = true
2080
2081     vb = Gtk::VBox.new(false, 5).set_border_width(5)
2082     vb.pack_start(Gtk::Label.new(msg), false, false)
2083
2084     vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
2085     vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
2086     if mode != 'one dir scan'
2087         vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2088     end
2089     if mode == 'web-album'
2090         vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2091         vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2092     end
2093     vb.pack_start(Gtk::HSeparator.new, false, false)
2094
2095     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2096     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2097     vb.pack_end(bottom, false, false)
2098
2099     directories = nil
2100     update_progression_title_pb1 = proc {
2101         if mode == 'web-album'
2102             set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2103         elsif mode != 'one dir scan'
2104             set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2105         else
2106             set_mainwindow_title(pb1_1.fraction)
2107         end
2108     }
2109
2110     infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2111     refresh_thread = Thread.new {
2112         directories_counter = 0
2113         while line = infopipe.gets
2114             msg 3, "infopipe got data: #{line}"
2115             if line =~ /^directories: (\d+), sizes: (\d+)/
2116                 directories = $1.to_f + 1
2117                 sizes = $2.to_f
2118             elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2119                 elements = $3.to_f + 1
2120                 if mode == 'web-album'
2121                     elements += sizes
2122                 end
2123                 element_counter = 0
2124                 gtk_thread_protect { pb1_1.fraction = 0 }
2125                 if mode != 'one dir scan'
2126                     newtext = utf8(full_src_dir_to_rel($1, $2))
2127                     newtext = '/' if newtext == ''
2128                     gtk_thread_protect { pb1_2.text = newtext }
2129                     directories_counter += 1
2130                     gtk_thread_protect {
2131                         pb1_2.fraction = directories_counter / directories
2132                         update_progression_title_pb1.call
2133                     }
2134                 end
2135             elsif line =~ /^processing element$/
2136                 element_counter += 1
2137                 gtk_thread_protect {
2138                     pb1_1.fraction = element_counter / elements
2139                     update_progression_title_pb1.call
2140                 }
2141             elsif line =~ /^processing size$/
2142                 element_counter += 1
2143                 gtk_thread_protect {
2144                     pb1_1.fraction = element_counter / elements
2145                     update_progression_title_pb1.call
2146                 }
2147             elsif line =~ /^finished processing sizes$/
2148                 gtk_thread_protect { pb1_1.fraction = 1 }
2149             elsif line =~ /^creating index.html$/
2150                 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2151                 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2152                 directories_counter = 0
2153             elsif line =~ /^index.html: (.+)\|(.+)/
2154                 newtext = utf8(full_src_dir_to_rel($1, $2))
2155                 newtext = '/' if newtext == ''
2156                 gtk_thread_protect { pb2.text = newtext }
2157                 directories_counter += 1
2158                 gtk_thread_protect {
2159                     pb2.fraction = directories_counter / directories
2160                     set_mainwindow_title(0.9 + pb2.fraction / 10)
2161                 }
2162             elsif line =~ /^die: (.*)$/
2163                 $diemsg = $1
2164             end
2165         end
2166     }
2167
2168     w.add(vb)
2169     w.signal_connect('delete-event') { w.destroy }
2170     w.signal_connect('destroy') {
2171         Thread.kill(refresh_thread)
2172         gtk_thread_flush  #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2173         if infopipe_path
2174             infopipe.close
2175             File.delete(infopipe_path)
2176         end
2177         set_mainwindow_title(nil)
2178     }
2179     w.window_position = Gtk::Window::POS_CENTER
2180     w.show_all
2181
2182     return [ b, w ]
2183 end
2184
2185 def call_backend(cmd, waitmsg, mode, params)
2186     pipe = Tempfile.new("boohpipe")
2187     Thread.critical = true
2188     path = pipe.path
2189     pipe.close!
2190     system("mkfifo #{path}")
2191     Thread.critical = false
2192     cmd += " --info-pipe #{path}"
2193     button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2194     pid = nil
2195     Thread.new {
2196         msg 2, cmd
2197         if pid = fork
2198             id, exitstatus = Process.waitpid2(pid)
2199             gtk_thread_protect { w8.destroy }
2200             if exitstatus == 0
2201                 if params[:successmsg]
2202                     gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2203                 end
2204                 if params[:closure_after]
2205                     gtk_thread_protect(&params[:closure_after])
2206                 end
2207             elsif exitstatus == 15
2208                 #- say nothing, user aborted
2209             else
2210                 gtk_thread_protect { show_popup($main_window,
2211                                                 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2212             end
2213         else
2214             exec(cmd)
2215         end
2216     }
2217     button.signal_connect('clicked') {
2218         Process.kill('SIGTERM', pid)
2219     }
2220 end
2221
2222 def save_changes(*forced)
2223     if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2224         return
2225     end
2226
2227     $xmldir.delete_attribute('already-generated')
2228
2229     propagate_children = proc { |xmldir|
2230         if xmldir.attributes['subdirs-caption']
2231             xmldir.delete_attribute('already-generated')
2232         end
2233         xmldir.elements.each('dir') { |element|
2234             propagate_children.call(element)
2235         }
2236     }
2237
2238     if $xmldir.child_byname_notattr('dir', 'deleted')
2239         new_title = $subalbums_title.buffer.text
2240         if new_title != $xmldir.attributes['subdirs-caption']
2241             parent = $xmldir.parent
2242             if parent.name == 'dir'
2243                 parent.delete_attribute('already-generated')
2244             end
2245             propagate_children.call($xmldir)
2246         end
2247         $xmldir.add_attribute('subdirs-caption', new_title)
2248         $xmldir.elements.each('dir') { |element|
2249             if !element.attributes['deleted']
2250                 path = element.attributes['path']
2251                 newtext = $subalbums_edits[path][:editzone].buffer.text
2252                 if element.attributes['subdirs-caption']
2253                     if element.attributes['subdirs-caption'] != newtext
2254                         propagate_children.call(element)
2255                     end
2256                     element.add_attribute('subdirs-caption',     newtext)
2257                     element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2258                 else
2259                     if element.attributes['thumbnails-caption'] != newtext
2260                         element.delete_attribute('already-generated')
2261                     end
2262                     element.add_attribute('thumbnails-caption',     newtext)
2263                     element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2264                 end
2265             end
2266         }
2267     end
2268
2269     if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2270         if $xmldir.attributes['thumbnails-caption']
2271             path = $xmldir.attributes['path']
2272             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2273         end
2274     elsif $xmldir.attributes['thumbnails-caption']
2275         $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2276     end
2277
2278     if $xmldir.attributes['thumbnails-caption']
2279         if edit = $subalbums_edits[$xmldir.attributes['path']]
2280             $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2281         end
2282     end
2283
2284     #- remove and reinsert elements to reflect new ordering
2285     saves = {}
2286     cpt = 0
2287     $xmldir.elements.each { |element|
2288         if element.name == 'image' || element.name == 'video'
2289             saves[element.attributes['filename']] = element.remove
2290             cpt += 1
2291         end
2292     }
2293     $autotable.current_order.each { |path|
2294         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2295         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2296         saves.delete(path)
2297     }
2298     saves.each_key { |path|
2299         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2300         chld.add_attribute('deleted', 'true')
2301     }
2302 end
2303
2304 def sort_by_exif_date
2305     $modified = true
2306     save_changes
2307     current_order = []
2308     rexml_thread_protect {
2309         $xmldir.elements.each { |element|
2310             if element.name == 'image' || element.name == 'video'
2311                 current_order << element.attributes['filename']
2312             end
2313         }
2314     }
2315
2316     #- look for EXIF dates
2317     dates = {}
2318
2319     if current_order.size > 20
2320         w = create_window
2321         w.set_transient_for($main_window)
2322         w.modal = true
2323         vb = Gtk::VBox.new(false, 5).set_border_width(5)
2324         vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2325         vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2326         bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2327         b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2328         vb.pack_end(bottom, false, false)
2329         w.add(vb)
2330         w.signal_connect('delete-event') { w.destroy }
2331         w.window_position = Gtk::Window::POS_CENTER
2332         w.show_all
2333
2334         aborted = false
2335         b.signal_connect('clicked') { aborted = true }
2336         i = 0
2337         current_order.each { |f|
2338             i += 1
2339             if entry2type(f) == 'image'
2340                 pb.text = f
2341                 pb.fraction = i.to_f / current_order.size
2342                 Gtk.main_iteration while Gtk.events_pending?
2343                 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2344                 if ! date_time.nil?
2345                     dates[f] = date_time
2346                 end
2347             end
2348             if aborted
2349                 break
2350             end
2351         }
2352         w.destroy
2353         if aborted
2354             return
2355         end
2356
2357     else
2358         current_order.each { |f|
2359             date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2360             if ! date_time.nil?
2361                 dates[f] = date_time
2362             end
2363         }
2364     end
2365
2366     saves = {}
2367     rexml_thread_protect {
2368         $xmldir.elements.each { |element|
2369             if element.name == 'image' || element.name == 'video'
2370                 saves[element.attributes['filename']] = element.remove
2371             end
2372         }
2373     }
2374
2375     neworder = smartsort(current_order, dates)
2376
2377     rexml_thread_protect {
2378         neworder.each { |f|
2379             $xmldir.add_element(saves[f].name, saves[f].attributes)
2380         }
2381     }
2382
2383     #- let the auto-table reflect new ordering
2384     change_dir
2385 end
2386
2387 def remove_all_captions
2388     $modified = true
2389     texts = {}
2390     $autotable.current_order.each { |path|
2391         texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2392         $name2widgets[File.basename(path)][:textview].buffer.text = ''
2393     }
2394     save_undo(_("remove all captions"),
2395               proc { |texts|
2396                   texts.each_key { |key|
2397                       $name2widgets[key][:textview].buffer.text = texts[key]
2398                   }
2399                   $notebook.set_page(1)
2400                   proc {
2401                       texts.each_key { |key|
2402                           $name2widgets[key][:textview].buffer.text = ''
2403                       }
2404                       $notebook.set_page(1)
2405                   }
2406               }, texts)
2407 end
2408
2409 def change_dir
2410     $selected_elements.each_key { |path|
2411         $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2412     }
2413     $autotable.clear
2414     $vbox2widgets = {}
2415     $name2widgets = {}
2416     $name2closures = {}
2417     $selected_elements = {}
2418     $cuts = []
2419     $multiple_dnd = []
2420     UndoHandler.cleanup
2421     $undo_tb.sensitive = $undo_mb.sensitive = false
2422     $redo_tb.sensitive = $redo_mb.sensitive = false
2423
2424     if !$current_path
2425         return
2426     end
2427
2428     $subalbums_vb.children.each { |chld|
2429         $subalbums_vb.remove(chld)
2430     }
2431     $subalbums = Gtk::Table.new(0, 0, true)
2432     current_y_sub_albums = 0
2433
2434     $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2435     $subalbums_edits = {}
2436     subalbums_counter = 0
2437     subalbums_edits_bypos = {}
2438
2439     add_subalbum = proc { |xmldir, counter|
2440         $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2441         subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2442         if xmldir == $xmldir
2443             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2444             captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2445             caption = xmldir.attributes['thumbnails-caption']
2446             infotype = 'thumbnails'
2447         else
2448             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2449             captionfile, caption = find_subalbum_caption_info(xmldir)
2450             infotype = find_subalbum_info_type(xmldir)
2451         end
2452         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2453         hbox = Gtk::HBox.new
2454         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2455         f = Gtk::Frame.new
2456         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2457
2458         img = nil
2459         my_gen_real_thumbnail = proc {
2460             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2461         }
2462
2463         if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2464             f.add(img = Gtk::Image.new)
2465             my_gen_real_thumbnail.call
2466         else
2467             f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2468         end
2469         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2470         $subalbums.attach(hbox,
2471                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2472
2473         frame, textview = create_editzone($subalbums_sw, 0, img)
2474         textview.buffer.text = caption
2475         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2476                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2477
2478         change_image = proc {
2479             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2480                                             nil,
2481                                             Gtk::FileChooser::ACTION_OPEN,
2482                                             nil,
2483                                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2484             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2485             fc.transient_for = $main_window
2486             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))
2487             f.add(preview_img = Gtk::Image.new)
2488             preview.show_all
2489             fc.signal_connect('update-preview') { |w|
2490                 if fc.preview_filename
2491                     if entry2type(fc.preview_filename) == 'video'
2492                         image_path = nil
2493                         tmpdir = nil
2494                         begin
2495                             tmpdir = gen_video_thumbnail(fc.preview_filename, false, 0)
2496                             if tmpdir.nil?
2497                                 fc.preview_widget_active = false
2498                             else
2499                                 tmpimage = "#{tmpdir}/00000001.jpg"
2500                                 begin
2501                                     preview_img.pixbuf = Gdk::Pixbuf.new(tmpimage, 240, 180)
2502                                     fc.preview_widget_active = true
2503                                 rescue Gdk::PixbufError
2504                                     fc.preview_widget_active = false
2505                                 ensure
2506                                     File.delete(tmpimage)
2507                                     Dir.rmdir(tmpdir)
2508                                 end
2509                             end
2510                         end
2511                     else
2512                         begin
2513                             preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2514                             fc.preview_widget_active = true
2515                         rescue Gdk::PixbufError
2516                             fc.preview_widget_active = false
2517                         end
2518                     end
2519                 end
2520             }
2521             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2522                 $modified = true
2523                 old_file = captionfile
2524                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2525                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2526                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2527                 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2528
2529                 new_file = fc.filename
2530                 msg 3, "new captionfile is: #{fc.filename}"
2531                 perform_changefile = proc {
2532                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2533                     $modified_pixbufs.delete(thumbnail_file)
2534                     xmldir.delete_attribute("#{infotype}-rotate")
2535                     xmldir.delete_attribute("#{infotype}-color-swap")
2536                     xmldir.delete_attribute("#{infotype}-enhance")
2537                     xmldir.delete_attribute("#{infotype}-seektime")
2538                     my_gen_real_thumbnail.call
2539                 }
2540                 perform_changefile.call
2541
2542                 save_undo(_("change caption file for sub-album"),
2543                           proc {
2544                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2545                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2546                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2547                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2548                               xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2549                               my_gen_real_thumbnail.call
2550                               $notebook.set_page(0)
2551                               proc {
2552                                   perform_changefile.call
2553                                   $notebook.set_page(0)
2554                               }
2555                           })
2556             end
2557             fc.destroy
2558         }
2559
2560         refresh = proc {
2561             if File.exists?(thumbnail_file)
2562                 File.delete(thumbnail_file)
2563             end
2564             my_gen_real_thumbnail.call
2565         }
2566
2567         rotate_and_cleanup = proc { |angle|
2568             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2569             if File.exists?(thumbnail_file)
2570                 File.delete(thumbnail_file)
2571             end
2572         }
2573
2574         move = proc { |direction|
2575             $modified = true
2576
2577             save_changes('forced')
2578             oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2579             if direction == 'up'
2580                 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2581                 subalbums_edits_bypos[oldpos - 1][:position] += 1
2582             end
2583             if direction == 'down'
2584                 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2585                 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2586             end
2587             if direction == 'top'
2588                 for i in 1 .. oldpos - 1
2589                     subalbums_edits_bypos[i][:position] += 1
2590                 end
2591                 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2592             end
2593             if direction == 'bottom'
2594                 for i in oldpos + 1 .. subalbums_counter
2595                     subalbums_edits_bypos[i][:position] -= 1
2596                 end
2597                 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2598             end
2599
2600             elems = []
2601             $xmldir.elements.each('dir') { |element|
2602                 if (!element.attributes['deleted'])
2603                     elems << [ element.attributes['path'], element.remove ]
2604                 end
2605             }
2606             elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2607                   each { |e| $xmldir.add_element(e[1]) }
2608             #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2609             $xmldir.elements.each('descendant::dir') { |elem|
2610                 elem.delete_attribute('already-generated')
2611             }
2612
2613             sel = $albums_tv.selection.selected_rows
2614             change_dir
2615             populate_subalbums_treeview(false)
2616             $albums_tv.selection.select_path(sel[0])
2617         }
2618
2619         color_swap_and_cleanup = proc {
2620             perform_color_swap_and_cleanup = proc {
2621                 color_swap(xmldir, "#{infotype}-")
2622                 my_gen_real_thumbnail.call
2623             }
2624             perform_color_swap_and_cleanup.call
2625
2626             save_undo(_("color swap"),
2627                       proc {
2628                           perform_color_swap_and_cleanup.call
2629                           $notebook.set_page(0)
2630                           proc {
2631                               perform_color_swap_and_cleanup.call
2632                               $notebook.set_page(0)
2633                           }
2634                       })
2635         }
2636
2637         change_seektime_and_cleanup = proc {
2638             if values = ask_new_seektime(xmldir, "#{infotype}-")
2639                 perform_change_seektime_and_cleanup = proc { |val|
2640                     change_seektime(xmldir, "#{infotype}-", val)
2641                     my_gen_real_thumbnail.call
2642                 }
2643                 perform_change_seektime_and_cleanup.call(values[:new])
2644
2645                 save_undo(_("specify seektime"),
2646                           proc {
2647                               perform_change_seektime_and_cleanup.call(values[:old])
2648                               $notebook.set_page(0)
2649                               proc {
2650                                   perform_change_seektime_and_cleanup.call(values[:new])
2651                                   $notebook.set_page(0)
2652                               }
2653                           })
2654             end
2655         }
2656
2657         whitebalance_and_cleanup = proc {
2658             if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2659                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2660                 perform_change_whitebalance_and_cleanup = proc { |val|
2661                     change_whitebalance(xmldir, "#{infotype}-", val)
2662                     recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2663                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2664                     if File.exists?(thumbnail_file)
2665                         File.delete(thumbnail_file)
2666                     end
2667                 }
2668                 perform_change_whitebalance_and_cleanup.call(values[:new])
2669                 
2670                 save_undo(_("fix white balance"),
2671                           proc {
2672                               perform_change_whitebalance_and_cleanup.call(values[:old])
2673                               $notebook.set_page(0)
2674                               proc {
2675                                   perform_change_whitebalance_and_cleanup.call(values[:new])
2676                                   $notebook.set_page(0)
2677                               }
2678                           })
2679             end
2680         }
2681
2682         gammacorrect_and_cleanup = proc {
2683             if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2684                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2685                 perform_change_gammacorrect_and_cleanup = proc { |val|
2686                     change_gammacorrect(xmldir, "#{infotype}-", val)
2687                     recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2688                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2689                     if File.exists?(thumbnail_file)
2690                         File.delete(thumbnail_file)
2691                     end
2692                 }
2693                 perform_change_gammacorrect_and_cleanup.call(values[:new])
2694                 
2695                 save_undo(_("gamma correction"),
2696                           proc {
2697                               perform_change_gammacorrect_and_cleanup.call(values[:old])
2698                               $notebook.set_page(0)
2699                               proc {
2700                                   perform_change_gammacorrect_and_cleanup.call(values[:new])
2701                                   $notebook.set_page(0)
2702                               }
2703                           })
2704             end
2705         }
2706
2707         enhance_and_cleanup = proc {
2708             perform_enhance_and_cleanup = proc {
2709                 enhance(xmldir, "#{infotype}-")
2710                 my_gen_real_thumbnail.call
2711             }
2712             
2713             perform_enhance_and_cleanup.call
2714             
2715             save_undo(_("enhance"),
2716                       proc {
2717                           perform_enhance_and_cleanup.call
2718                           $notebook.set_page(0)
2719                           proc {
2720                               perform_enhance_and_cleanup.call
2721                               $notebook.set_page(0)
2722                           }
2723                       })
2724         }
2725
2726         evtbox.signal_connect('button-press-event') { |w, event|
2727             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2728                 if $r90.active?
2729                     rotate_and_cleanup.call(90)
2730                 elsif $r270.active?
2731                     rotate_and_cleanup.call(-90)
2732                 elsif $enhance.active?
2733                     enhance_and_cleanup.call
2734                 end
2735             end
2736             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2737                 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2738                                      { :forbid_left => true, :forbid_right => true,
2739                                        :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2740                                        :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2741                                      { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2742                                        :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2743                                        :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2744             end
2745             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2746                 change_image.call
2747                 true   #- handled
2748             end
2749         }
2750         evtbox.signal_connect('button-press-event') { |w, event|
2751             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2752             false
2753         }
2754
2755         evtbox.signal_connect('button-release-event') { |w, event|
2756             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2757                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2758                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2759                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2760                     msg 3, "gesture rotate: #{angle}"
2761                     rotate_and_cleanup.call(angle)
2762                 end
2763             end
2764             $gesture_press = nil
2765         }
2766                 
2767         $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2768         $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2769         current_y_sub_albums += 1
2770     }
2771
2772     if $xmldir.child_byname_notattr('dir', 'deleted')
2773         #- title edition
2774         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2775         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] || ''
2776         $subalbums_title.set_justification(Gtk::Justification::CENTER)
2777         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2778         #- this album image/caption
2779         if $xmldir.attributes['thumbnails-caption']
2780             add_subalbum.call($xmldir, 0)
2781         end
2782     end
2783     total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2784     $xmldir.elements.each { |element|
2785         if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2786             #- element (image or video) of this album
2787             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2788             msg 3, "dest_img: #{dest_img}"
2789             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2790             total[element.name] += 1
2791         end
2792         if element.name == 'dir' && !element.attributes['deleted']
2793             #- sub-album image/caption
2794             add_subalbum.call(element, subalbums_counter += 1)
2795             total[element.name] += 1
2796         end
2797     }
2798     $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2799                                                                                 total['image'], total['video'], total['dir'] ]))
2800     $subalbums_vb.add($subalbums)
2801     $subalbums_vb.show_all
2802
2803     if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2804         $notebook.get_tab_label($autotable_sw).sensitive = false
2805         $notebook.set_page(0)
2806         $thumbnails_title.buffer.text = ''
2807     else
2808         $notebook.get_tab_label($autotable_sw).sensitive = true
2809         $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2810     end
2811
2812     if !$xmldir.child_byname_notattr('dir', 'deleted')
2813         $notebook.get_tab_label($subalbums_sw).sensitive = false
2814         $notebook.set_page(1)
2815     else
2816         $notebook.get_tab_label($subalbums_sw).sensitive = true
2817     end
2818 end
2819
2820 def pixbuf_or_nil(filename)
2821     begin
2822         return Gdk::Pixbuf.new(filename)
2823     rescue
2824         return nil
2825     end
2826 end
2827
2828 def theme_choose(current)
2829     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2830                              $main_window,
2831                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2832                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2833                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2834
2835     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2836     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2837     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2838     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2839     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2840     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2841     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2842     treeview.signal_connect('button-press-event') { |w, event|
2843         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2844             dialog.response(Gtk::Dialog::RESPONSE_OK)
2845         end
2846     }
2847
2848     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2849
2850     ([ $FPATH + '/themes/simple' ] + (`find '#{$FPATH}/themes' ~/.booh-themes -mindepth 1 -maxdepth 1 -type d 2>/dev/null`.find_all { |e| e !~ /simple$/ }.sort)).each { |dir|
2851         dir.chomp!
2852         iter = model.append
2853         iter[0] = File.basename(dir)
2854         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2855         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2856         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2857         if File.basename(dir) == current
2858             treeview.selection.select_iter(iter)
2859         end
2860     }
2861     dialog.set_default_size(-1, 500)
2862     dialog.vbox.show_all
2863
2864     dialog.run { |response|
2865         iter = treeview.selection.selected
2866         dialog.destroy
2867         if response == Gtk::Dialog::RESPONSE_OK && iter
2868             return model.get_value(iter, 0)
2869         end
2870     }
2871     return nil
2872 end
2873
2874 def show_password_protections
2875     examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2876         child_iter = $albums_iters[xmldir.attributes['path']]
2877         if xmldir.attributes['password-protect']
2878             child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2879             already_protected = true
2880         elsif already_protected
2881             pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2882             if pix
2883                 pix = pix.saturate_and_pixelate(1, true)
2884             end
2885             child_iter[2] = pix
2886         else
2887             child_iter[2] = nil
2888         end
2889         xmldir.elements.each('dir') { |elem|
2890             if !elem.attributes['deleted']
2891                 examine_dir_elem.call(child_iter, elem, already_protected)
2892             end
2893         }
2894     }
2895     examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2896 end
2897
2898 def populate_subalbums_treeview(select_first)
2899     $albums_ts.clear
2900     $autotable.clear
2901     $albums_iters = {}
2902     $subalbums_vb.children.each { |chld|
2903         $subalbums_vb.remove(chld)
2904     }
2905
2906     source = $xmldoc.root.attributes['source']
2907     msg 3, "source: #{source}"
2908
2909     xmldir = $xmldoc.elements['//dir']
2910     if !xmldir || xmldir.attributes['path'] != source
2911         msg 1, _("Corrupted booh file...")
2912         return
2913     end
2914
2915     append_dir_elem = proc { |parent_iter, xmldir|
2916         child_iter = $albums_ts.append(parent_iter)
2917         child_iter[0] = File.basename(xmldir.attributes['path'])
2918         child_iter[1] = xmldir.attributes['path']
2919         $albums_iters[xmldir.attributes['path']] = child_iter
2920         msg 3, "puttin location: #{xmldir.attributes['path']}"
2921         xmldir.elements.each('dir') { |elem|
2922             if !elem.attributes['deleted']
2923                 append_dir_elem.call(child_iter, elem)
2924             end
2925         }
2926     }
2927     append_dir_elem.call(nil, xmldir)
2928     show_password_protections
2929
2930     $albums_tv.expand_all
2931     if select_first
2932         $albums_tv.selection.select_iter($albums_ts.iter_first)
2933     end
2934 end
2935
2936 def select_current_theme
2937     select_theme($xmldoc.root.attributes['theme'],
2938                  $xmldoc.root.attributes['limit-sizes'],
2939                  !$xmldoc.root.attributes['optimize-for-32'].nil?,
2940                  $xmldoc.root.attributes['thumbnails-per-row'])
2941 end
2942
2943 def open_file(filename)
2944
2945     $filename = nil
2946     $modified = false
2947     $current_path = nil   #- invalidate
2948     $modified_pixbufs = {}
2949     $albums_ts.clear
2950     $autotable.clear
2951     $subalbums_vb.children.each { |chld|
2952         $subalbums_vb.remove(chld)
2953     }
2954
2955     if !File.exists?(filename)
2956         return utf8(_("File not found."))
2957     end
2958
2959     begin
2960         $xmldoc = REXML::Document.new(File.new(filename))
2961     rescue Exception
2962         $xmldoc = nil
2963     end
2964
2965     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2966         if entry2type(filename).nil?
2967             return utf8(_("Not a booh file!"))
2968         else
2969             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."))
2970         end
2971     end
2972
2973     if !source = $xmldoc.root.attributes['source']
2974         return utf8(_("Corrupted booh file..."))
2975     end
2976
2977     if !dest = $xmldoc.root.attributes['destination']
2978         return utf8(_("Corrupted booh file..."))
2979     end
2980
2981     if !theme = $xmldoc.root.attributes['theme']
2982         return utf8(_("Corrupted booh file..."))
2983     end
2984
2985     if $xmldoc.root.attributes['version'] < $VERSION
2986         msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2987         mark_document_as_dirty
2988         if $xmldoc.root.attributes['version'] < '0.8.4'
2989             msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2990             `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2991                 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2992                 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2993                 if old_dest_dir != new_dest_dir
2994                     sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2995                 end
2996                 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2997                     xmldir.elements.each { |element|
2998                         if %w(image video).include?(element.name) && !element.attributes['deleted']
2999                             old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
3000                             new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
3001                             Dir[old_name + '*'].each { |file|
3002                                 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
3003                                 file != new_file and sys("mv '#{file}' '#{new_file}'")
3004                             }
3005                         end
3006                         if element.name == 'dir' && !element.attributes['deleted']
3007                             old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
3008                             new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
3009                             old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
3010                         end
3011                     }
3012                 else
3013                     msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
3014                 end
3015             }
3016         end
3017         $xmldoc.root.add_attribute('version', $VERSION)
3018     end
3019
3020     select_current_theme
3021
3022     $filename = filename
3023     set_mainwindow_title(nil)
3024     $default_size['thumbnails'] =~ /(.*)x(.*)/
3025     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3026     $albums_thumbnail_size =~ /(.*)x(.*)/
3027     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3028
3029     populate_subalbums_treeview(true)
3030
3031     $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $extend.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
3032     return nil
3033 end
3034
3035 def open_file_user(filename)
3036     result = open_file(filename)
3037     if !result
3038         $config['last-opens'] ||= []
3039         if $config['last-opens'][-1] != utf8(filename)
3040             $config['last-opens'] << utf8(filename)
3041         end
3042         $orig_filename = $filename
3043         $main_window.title = 'booh - ' + File.basename($orig_filename)
3044         tmp = Tempfile.new("boohtemp")
3045         Thread.critical = true
3046         $filename = tmp.path
3047         tmp.close!
3048         #- for security
3049         ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
3050         Thread.critical = false
3051         ios.close
3052         $tempfiles << $filename << "#{$filename}.backup"
3053     else
3054         $orig_filename = nil
3055     end
3056     return result
3057 end
3058
3059 def open_file_popup
3060     if !ask_save_modifications(utf8(_("Save this album?")),
3061                                utf8(_("Do you want to save the changes to this album?")),
3062                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3063         return
3064     end
3065     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
3066                                     nil,
3067                                     Gtk::FileChooser::ACTION_OPEN,
3068                                     nil,
3069                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3070     fc.add_shortcut_folder(File.expand_path("~/.booh"))
3071     fc.set_current_folder(File.expand_path("~/.booh"))
3072     fc.transient_for = $main_window
3073     fc.preview_widget = previewlabel = Gtk::Label.new.show
3074     fc.signal_connect('update-preview') { |w|
3075         if fc.preview_filename
3076             begin
3077                 push_mousecursor_wait(fc)
3078                 xmldoc = REXML::Document.new(File.new(fc.preview_filename))
3079                 subalbums = 0
3080                 images = 0
3081                 videos = 0
3082                 xmldoc.elements.each('//*') { |elem|
3083                     if elem.name == 'dir'
3084                         subalbums += 1
3085                     elsif elem.name == 'image'
3086                         images += 1
3087                     elsif elem.name == 'video'
3088                         videos += 1
3089                     end
3090                 }
3091             rescue Exception
3092             ensure
3093                 pop_mousecursor(fc)
3094             end
3095             if !xmldoc || !xmldoc.root || xmldoc.root.name != 'booh'
3096                 fc.preview_widget_active = false
3097             else
3098                 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") %
3099                                            [ xmldoc.root.attributes['source'], xmldoc.root.attributes['destination'], subalbums, images, videos ])
3100                 fc.preview_widget_active = true
3101             end
3102         end
3103     }
3104     ok = false
3105     while !ok
3106         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3107             push_mousecursor_wait(fc)
3108             msg = open_file_user(fc.filename)
3109             pop_mousecursor(fc)
3110             if msg
3111                 show_popup(fc, msg)
3112                 ok = false
3113             else
3114                 ok = true
3115             end
3116         else
3117             ok = true
3118         end
3119     end
3120     fc.destroy
3121 end
3122
3123 def additional_booh_options
3124     options = ''
3125     if $config['mproc']
3126         options += "--mproc #{$config['mproc'].to_i} "
3127     end
3128     options += "--comments-format '#{$config['comments-format']}' "
3129     if $config['transcode-videos']
3130         options += "--transcode-videos '#{$config['transcode-videos']}' "
3131     end
3132     if $config['use-flv'] == 'true'
3133         options += "--flv-generator '#{$config['flv-generator']}' "
3134     end
3135     return options
3136 end
3137
3138 def ask_multi_languages(value)
3139     if ! value.nil?
3140         spl = value.split(',')
3141         value = [ spl[0..-2], spl[-1] ]
3142     end
3143
3144     dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
3145                              $main_window,
3146                              Gtk::Dialog::MODAL,
3147                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3148                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3149
3150     lbl = Gtk::Label.new
3151     lbl.markup = utf8(
3152 _("You can choose to activate <b>multi-languages</b> support for this web-album
3153 (it will work only if you publish your web-album on an Apache web-server). This will
3154 use the MultiViews feature of Apache; the pages will be served according to the
3155 value of the Accept-Language HTTP header sent by the web browsers, so that people
3156 with different languages preferences will be able to browse your web-album with
3157 navigation in their language (if language is available).
3158 "))
3159
3160     dialog.vbox.add(lbl)
3161     dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3162                                                                          add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3163                                                                                                      add(languages = Gtk::Button.new))))
3164
3165     pick_languages = proc {
3166         dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3167                                   $main_window,
3168                                   Gtk::Dialog::MODAL,
3169                                   [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3170                                   [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3171
3172         dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3173         hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3174         cbs = []
3175         SUPPORTED_LANGUAGES.each { |lang|
3176             hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3177             if ! value.nil? && value[0].include?(lang)
3178                 cb.active = true
3179             end
3180             cbs << [ lang, cb ]
3181         }
3182
3183         dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3184         hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3185         fallback_language = nil
3186         hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3187         fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3188         if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3189             fbl_rb.active = true
3190             fallback_language = SUPPORTED_LANGUAGES[0]
3191         end
3192         SUPPORTED_LANGUAGES[1..-1].each { |lang|
3193             hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3194             rb.signal_connect('clicked') { fallback_language = lang }
3195             if ! value.nil? && value[1] == lang
3196                 rb.active = true
3197             end
3198         }
3199
3200         dialog2.window_position = Gtk::Window::POS_MOUSE
3201         dialog2.show_all
3202
3203         resp = nil
3204         dialog2.run { |response|
3205             resp = response
3206             if resp == Gtk::Dialog::RESPONSE_OK
3207                 value = []
3208                 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3209                 value[1] = fallback_language
3210                 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3211             end
3212             dialog2.destroy
3213         }
3214         resp
3215     }
3216
3217     languages.signal_connect('clicked') {
3218         pick_languages.call
3219     }
3220     dialog.window_position = Gtk::Window::POS_MOUSE
3221     if value.nil?
3222         rb_no.active = true
3223     else
3224         rb_yes.active = true
3225         languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3226     end
3227     rb_no.signal_connect('clicked') {
3228         if rb_no.active?
3229             languages.hide
3230         else
3231             if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3232                 rb_no.activate
3233             else
3234                 languages.show
3235             end
3236         end
3237     }
3238     oldval = value
3239     dialog.show_all
3240     if rb_no.active?
3241         languages.hide
3242     end
3243
3244     dialog.run { |response|
3245         if rb_no.active?
3246             value = nil
3247         end
3248         dialog.destroy
3249         if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3250             if value.nil?
3251                 return [ true, nil ]
3252             else
3253                 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3254             end
3255         else
3256             return [ false ]
3257         end
3258     }
3259 end
3260
3261 def new_album
3262     if !ask_save_modifications(utf8(_("Save this album?")),
3263                                utf8(_("Do you want to save the changes to this album?")),
3264                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3265         return
3266     end
3267     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3268                              $main_window,
3269                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3270                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3271                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3272     
3273     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3274     tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3275                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3276     tbl.attach(src = Gtk::Entry.new,
3277                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3278     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3279                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3280     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3281                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3282     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3283                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3284     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3285                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3286     tbl.attach(dest = Gtk::Entry.new,
3287                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3288     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3289                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3290     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3291                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3292     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3293                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3294     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3295                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3296
3297     tooltips = Gtk::Tooltips.new
3298     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3299     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3300                          pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3301     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3302                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3303     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3304     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)
3305     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3306                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3307     nperpage_model = Gtk::ListStore.new(String, String)
3308     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3309                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3310     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3311     nperpagecombo.set_attributes(crt, { :markup => 0 })
3312     iter = nperpage_model.append
3313     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3314     iter[1] = nil
3315     [ 12, 20, 30, 40, 50 ].each { |v|
3316         iter = nperpage_model.append
3317         iter[0] = iter[1] = v.to_s
3318     }
3319     nperpagecombo.active = 0
3320
3321     multilanguages_value = nil
3322     vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3323                                         pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3324     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)
3325     multilanguages.signal_connect('clicked') {
3326         retval = ask_multi_languages(multilanguages_value)
3327         if retval[0] 
3328             multilanguages_value = retval[1]
3329         end
3330         if multilanguages_value
3331             ml_label.text = utf8(_("Multi-languages: enabled."))
3332         else
3333             ml_label.text = utf8(_("Multi-languages: disabled."))
3334         end
3335     }
3336     if $config['default-multi-languages']
3337         multilanguages_value = $config['default-multi-languages']
3338         ml_label.text = utf8(_("Multi-languages: enabled."))
3339     else
3340         ml_label.text = utf8(_("Multi-languages: disabled."))
3341     end
3342
3343     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3344                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3345     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)
3346     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3347                                    pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3348     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)
3349     vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active($config['default-addthis'].to_b))
3350     vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active($config['default-quotehtml'].to_b))
3351     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)
3352
3353     src_nb_calculated_for = ''
3354     src_nb_process = nil
3355     process_src_nb = proc {
3356         if src.text != src_nb_calculated_for
3357             src_nb_calculated_for = src.text
3358             if src_nb_process
3359                 begin
3360                     Process.kill(9, src_nb_process)
3361                 rescue Errno::ESRCH
3362                     #- process doesn't exist anymore - race condition
3363                 end
3364             end
3365             if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3366                 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3367             else
3368                 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3369                     if File.readable?(from_utf8_safe(src_nb_calculated_for))
3370                         rd, wr = IO.pipe
3371                         if src_nb_process
3372                             while src_nb_process
3373                                 msg 3, "sleeping for completion of previous process"
3374                                 sleep 0.05
3375                             end
3376                             gtk_thread_flush  #- flush to avoid race condition in src_nb markup update
3377                         end
3378                         src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
3379                         total = { 'image' => 0, 'video' => 0, nil => 0 }
3380                         if src_nb_process = fork
3381                             msg 3, "spawned #{src_nb_process} for #{src_nb_calculated_for}"
3382                             #- parent
3383                             wr.close
3384                             Thread.new {
3385                                 rd.readlines.each { |dir|
3386                                     if File.basename(dir) =~ /^\./
3387                                         next
3388                                     else
3389                                         begin
3390                                             Dir.entries(dir.chomp).each { |file|
3391                                                 total[entry2type(file)] += 1
3392                                             }
3393                                         rescue Errno::EACCES, Errno::ENOENT
3394                                         end
3395                                     end
3396                                 }
3397                                 rd.close
3398                                 msg 3, "ripping #{src_nb_process}"
3399                                 dummy, exitstatus = Process.waitpid2(src_nb_process)
3400                                 if exitstatus == 0
3401                                     gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3402                                 end
3403                                 src_nb_process = nil
3404                             }
3405                             
3406                         else
3407                             #- child
3408                             rd.close
3409                             wr.write(`find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`)
3410                             Process.exit!(0)  #- _exit
3411                         end                       
3412                     else
3413                         src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3414                     end
3415                 else
3416                     src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3417                 end
3418             end
3419         end
3420         true
3421     }
3422     timeout_src_nb = Gtk.timeout_add(100) {
3423         process_src_nb.call
3424     }
3425
3426     src_browse.signal_connect('clicked') {
3427         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3428                                         nil,
3429                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
3430                                         nil,
3431                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3432         fc.transient_for = $main_window
3433         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3434             src.text = utf8(fc.filename)
3435             process_src_nb.call
3436             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3437         end
3438         fc.destroy
3439     }
3440
3441     dest_browse.signal_connect('clicked') {
3442         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3443                                         nil,
3444                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
3445                                         nil,
3446                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3447         fc.transient_for = $main_window
3448         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3449             dest.text = utf8(fc.filename)
3450         end
3451         fc.destroy
3452     }
3453
3454     conf_browse.signal_connect('clicked') {
3455         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3456                                         nil,
3457                                         Gtk::FileChooser::ACTION_SAVE,
3458                                         nil,
3459                                         [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3460         fc.transient_for = $main_window
3461         fc.add_shortcut_folder(File.expand_path("~/.booh"))
3462         fc.set_current_folder(File.expand_path("~/.booh"))
3463         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3464             conf.text = utf8(fc.filename)
3465         end
3466         fc.destroy
3467     }
3468
3469     theme_sizes = []
3470     nperrows = []
3471     recreate_theme_config = proc {
3472         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3473         theme_sizes = []
3474         select_theme(theme_button.label, 'all', optimize432.active?, nil)
3475         $images_size.each { |s|
3476             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3477             if !s['optional']
3478                 cb.active = true
3479             end
3480             tooltips.set_tip(cb, utf8(s['description']), nil)
3481             theme_sizes << { :widget => cb, :value => s['name'] }
3482         }
3483         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3484         tooltips = Gtk::Tooltips.new
3485         tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3486         theme_sizes << { :widget => cb, :value => 'original' }
3487         sizes.show_all
3488
3489         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3490         nperrow_group = nil
3491         nperrows = []
3492         $allowed_N_values.each { |n|
3493             if nperrow_group
3494                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3495             else
3496                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3497             end
3498             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3499             if $default_N == n
3500                 rb.active = true
3501             end
3502             nperrows << { :widget => rb, :value => n }
3503         }
3504         nperrowradios.show_all
3505     }
3506     recreate_theme_config.call
3507
3508     theme_button.signal_connect('clicked') {
3509         if newtheme = theme_choose(theme_button.label)
3510             theme_button.label = newtheme
3511             recreate_theme_config.call
3512         end
3513     }
3514
3515     dialog.vbox.add(frame1)
3516     dialog.vbox.add(frame2)
3517     dialog.show_all
3518
3519     keepon = true
3520     ok = true
3521     while keepon
3522         dialog.run { |response|
3523             if response == Gtk::Dialog::RESPONSE_OK
3524                 srcdir = from_utf8_safe(src.text)
3525                 destdir = from_utf8_safe(dest.text)
3526                 confpath = from_utf8_safe(conf.text)
3527                 if src.text != '' && srcdir == ''
3528                     show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3529                     src.grab_focus
3530                 elsif !File.directory?(srcdir)
3531                     show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3532                     src.grab_focus
3533                 elsif dest.text != '' && destdir == ''
3534                     show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3535                     dest.grab_focus
3536                 elsif destdir != make_dest_filename(destdir)
3537                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3538                     dest.grab_focus
3539                 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3540                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3541 inside it will be permanently removed before creating the web-album!
3542 Are you sure you want to continue?")), { :okcancel => true })
3543                     dest.grab_focus
3544                 elsif File.exists?(destdir) && !File.directory?(destdir)
3545                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3546                     dest.grab_focus
3547                 elsif conf.text == ''
3548                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3549                     conf.grab_focus
3550                 elsif conf.text != '' && confpath == ''
3551                     show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3552                     conf.grab_focus
3553                 elsif File.directory?(confpath)
3554                     show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3555                     conf.grab_focus
3556                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3557                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3558                 else
3559                     system("mkdir '#{destdir}'")
3560                     if !File.directory?(destdir)
3561                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3562                         dest.grab_focus
3563                     else
3564                         keepon = false
3565                     end
3566                 end
3567             else
3568                 keepon = ok = false
3569             end
3570         }
3571     end
3572     if ok
3573         srcdir = from_utf8(src.text)
3574         destdir = from_utf8(dest.text)
3575         configskel = File.expand_path(from_utf8(conf.text))
3576         theme = theme_button.label
3577         #- some sort of automatic theme preference
3578         $config['default-theme'] = theme
3579         $config['default-multi-languages'] = multilanguages_value
3580         $config['default-optimize32'] = optimize432.active?.to_s
3581         $config['default-addthis'] = addthis.active?.to_s
3582         $config['default-quotehtml'] = quotehtml.active?.to_s
3583         sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3584         nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3585         nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3586         opt432 = optimize432.active?
3587         madewith = madewithentry.text.gsub('\'', '&#39;')  #- because the parameters to booh-backend are between apostrophes
3588         indexlink = indexlinkentry.text.gsub('\'', '&#39;')
3589         athis = addthis.active?
3590         qhtml = quotehtml.active?
3591     end
3592     if src_nb_process
3593         begin
3594             Process.kill(9, src_nb_process)
3595             while src_nb_process
3596                 msg 3, "sleeping for completion of previous process"
3597                 sleep 0.05
3598             end
3599         rescue Errno::ESRCH
3600             #- process doesn't exist
3601         end
3602         gtk_thread_flush  #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3603     end
3604     dialog.destroy
3605     Gtk.timeout_remove(timeout_src_nb)
3606
3607     if ok
3608         call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3609                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3610                      (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3611                      (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
3612                      "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' " +
3613                      "#{athis ? '--addthis' : ''} #{qhtml ? '--quote-html' : ''} #{additional_booh_options}",
3614                      utf8(_("Please wait while scanning source directory...")),
3615                      'full scan',
3616                      { :closure_after => proc {
3617                              open_file_user(configskel)
3618                              $main_window.urgency_hint = true
3619                          } })
3620     end
3621 end
3622
3623 def properties
3624     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3625                              $main_window,
3626                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3627                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3628                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3629     
3630     source = $xmldoc.root.attributes['source']
3631     dest = $xmldoc.root.attributes['destination']
3632     theme = $xmldoc.root.attributes['theme']
3633     opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3634     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3635     nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3636     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3637     if limit_sizes
3638         limit_sizes = limit_sizes.split(/,/)
3639     end
3640     madewith = ($xmldoc.root.attributes['made-with'] || '').gsub('&#39;', '\'')
3641     indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub('&#39;', '\'')
3642     athis = !$xmldoc.root.attributes['addthis'].nil?
3643     qhtml = !$xmldoc.root.attributes['quote-html'].nil?
3644     save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3645
3646     tooltips = Gtk::Tooltips.new
3647     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3648     tbl.attach(Gtk::Label.new(utf8(_("Directory of source photos/videos: "))),
3649                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3650     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3651                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3652     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3653                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3654     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3655                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3656     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3657                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3658     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3659                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3660
3661     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3662     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3663                                    pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3664     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3665                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3666     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3667     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)
3668     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3669                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3670     nperpage_model = Gtk::ListStore.new(String, String)
3671     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3672                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3673     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3674     nperpagecombo.set_attributes(crt, { :markup => 0 })
3675     iter = nperpage_model.append
3676     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3677     iter[1] = nil
3678     [ 12, 20, 30, 40, 50 ].each { |v|
3679         iter = nperpage_model.append
3680         iter[0] = iter[1] = v.to_s
3681         if nperpage && nperpage == v.to_s
3682             nperpagecombo.active_iter = iter
3683         end
3684     }
3685     if nperpagecombo.active_iter.nil?
3686         nperpagecombo.active = 0
3687     end
3688
3689     vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3690                                         pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3691     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)
3692     ml_update = proc {
3693         if save_multilanguages_value
3694             ml_label.text = utf8(_("Multi-languages: enabled."))
3695         else
3696             ml_label.text = utf8(_("Multi-languages: disabled."))
3697         end
3698     }
3699     ml_update.call
3700     multilanguages.signal_connect('clicked') {
3701         retval = ask_multi_languages(save_multilanguages_value)
3702         if retval[0] 
3703             save_multilanguages_value = retval[1]
3704         end
3705         ml_update.call
3706     }
3707
3708     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3709                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3710     if indexlink
3711         indexlinkentry.text = indexlink
3712     end
3713     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)
3714     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3715                                    pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3716     if madewith
3717         madewithentry.text = madewith
3718     end
3719     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)
3720     vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active(athis))
3721     vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active(qhtml))
3722     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)
3723
3724     theme_sizes = []
3725     nperrows = []
3726     recreate_theme_config = proc {
3727         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3728         theme_sizes = []
3729         select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3730
3731         $images_size.each { |s|
3732             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3733             if limit_sizes
3734                 if limit_sizes.include?(s['name'])
3735                     cb.active = true
3736                 end
3737             else
3738                 if !s['optional']
3739                     cb.active = true
3740                 end
3741             end
3742             tooltips.set_tip(cb, utf8(s['description']), nil)
3743             theme_sizes << { :widget => cb, :value => s['name'] }
3744         }
3745         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3746         tooltips = Gtk::Tooltips.new
3747         tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3748         if limit_sizes && limit_sizes.include?('original')
3749             cb.active = true
3750         end
3751         theme_sizes << { :widget => cb, :value => 'original' }
3752         sizes.show_all
3753
3754         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3755         nperrow_group = nil
3756         nperrows = []
3757         $allowed_N_values.each { |n|
3758             if nperrow_group
3759                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3760             else
3761                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3762             end
3763             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3764             nperrowradios.add(Gtk::Label.new('  '))
3765             if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3766                 rb.active = true
3767             end
3768             nperrows << { :widget => rb, :value => n.to_s }
3769         }
3770         nperrowradios.show_all
3771     }
3772     recreate_theme_config.call
3773
3774     theme_button.signal_connect('clicked') {
3775         if newtheme = theme_choose(theme_button.label)
3776             limit_sizes = nil
3777             nperrow = nil
3778             theme_button.label = newtheme
3779             recreate_theme_config.call
3780         end
3781     }
3782
3783     dialog.vbox.add(frame1)
3784     dialog.vbox.add(frame2)
3785     dialog.show_all
3786
3787     keepon = true
3788     ok = true
3789     while keepon
3790         dialog.run { |response|
3791             if response == Gtk::Dialog::RESPONSE_OK
3792                 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3793                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3794                 else
3795                     keepon = false
3796                 end
3797             else
3798                 keepon = ok = false
3799             end
3800         }
3801     end
3802     save_theme = theme_button.label
3803     save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3804     save_opt432 = optimize432.active?
3805     save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3806     save_nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3807     save_madewith = madewithentry.text.gsub('\'', '&#39;')  #- because the parameters to booh-backend are between apostrophes
3808     save_indexlink = indexlinkentry.text.gsub('\'', '&#39;')
3809     save_addthis = addthis.active?
3810     save_quotehtml = quotehtml.active?
3811     dialog.destroy
3812     
3813     if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_nperpage != nperpage || save_madewith != madewith || save_indexlink != indexlink || save_multilanguages_value != multilanguages_value || save_quotehtml != qhtml || save_addthis != athis)
3814         #- some sort of automatic preferences
3815         if save_theme != theme
3816             $config['default-theme'] = save_theme
3817         end
3818         if save_multilanguages_value != multilanguages_value
3819             $config['default-multi-languages'] = save_multilanguages_value
3820         end
3821         if save_opt432 != opt432
3822             $config['default-optimize32'] = save_opt432.to_s
3823         end
3824         if save_addthis != athis
3825             $config['default-addthis'] = save_addthis.to_s
3826         end
3827         if save_quotehtml != qhtml
3828             $config['default-quotehtml'] = save_quotehtml.to_s
3829         end
3830         mark_document_as_dirty
3831         save_current_file
3832         call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3833                      "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3834                      (save_nperpage ? "--thumbnails-per-page #{save_nperpage} " : '') +
3835                      (save_multilanguages_value ? "--multi-languages #{save_multilanguages_value} " : '') +
3836                      "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' " +
3837                      "#{save_addthis ? '--addthis' : ''} #{save_quotehtml ? '--quote-html' : ''} #{additional_booh_options}",
3838                      utf8(_("Please wait while scanning source directory...")),
3839                      'full scan',
3840                      { :closure_after => proc {
3841                              open_file($filename)
3842                              $modified = true
3843                              $main_window.urgency_hint = true
3844                          } })
3845     else
3846         #- select_theme merges global variables, need to return to current choices
3847         select_current_theme
3848     end
3849 end
3850
3851 def merge_current
3852     save_current_file
3853
3854     sel = $albums_tv.selection.selected_rows
3855
3856     call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3857                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
3858                  utf8(_("Please wait while scanning source directory...")),
3859                  'one dir scan',
3860                  { :closure_after => proc {
3861                          open_file($filename)
3862                          $albums_tv.selection.select_path(sel[0])
3863                          $modified = true
3864                          $main_window.urgency_hint = true
3865                      } })
3866 end
3867
3868 def merge_newsubs
3869     save_current_file
3870
3871     sel = $albums_tv.selection.selected_rows
3872
3873     call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3874                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
3875                  utf8(_("Please wait while scanning source directory...")),
3876                  'subdirs scan',
3877                  { :closure_after => proc {
3878                          open_file($filename)
3879                          $albums_tv.selection.select_path(sel[0])
3880                          $modified = true
3881                          $main_window.urgency_hint = true
3882                      } })
3883 end
3884
3885 def merge
3886     save_current_file
3887
3888     theme = $xmldoc.root.attributes['theme']
3889     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3890     if limit_sizes
3891         limit_sizes = "--sizes #{limit_sizes}"
3892     end
3893     call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3894                  "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3895                  utf8(_("Please wait while scanning source directory...")),
3896                  'full scan',
3897                  { :closure_after => proc {
3898                          open_file($filename)
3899                          $modified = true
3900                          $main_window.urgency_hint = true
3901                      } })
3902 end
3903
3904 def save_as_do
3905     fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3906                                     nil,
3907                                     Gtk::FileChooser::ACTION_SAVE,
3908                                     nil,
3909                                     [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3910     fc.transient_for = $main_window
3911     fc.add_shortcut_folder(File.expand_path("~/.booh"))
3912     fc.set_current_folder(File.expand_path("~/.booh"))
3913     fc.filename = $orig_filename
3914     if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3915         $orig_filename = fc.filename
3916         if ! save_current_file_user
3917             fc.destroy
3918             return save_as_do
3919         end
3920         $config['last-opens'] ||= []
3921         $config['last-opens'] << $orig_filename
3922     end
3923     fc.destroy
3924 end
3925
3926 def preferences
3927     dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3928                              $main_window,
3929                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3930                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3931                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3932
3933     table_counter = 0
3934     dialog.vbox.add(notebook = Gtk::Notebook.new)
3935     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3936     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3937                0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3938     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer']).set_size_request(250, -1)),
3939                1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3940     tooltips = Gtk::Tooltips.new
3941     tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3942 for example: /usr/bin/mplayer %f")), nil)
3943
3944     table_counter += 1
3945     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3946                0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3947     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3948                1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3949     tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3950 for example: /usr/bin/gimp-remote %f")), nil)
3951
3952     table_counter += 1
3953     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3954                0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3955     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3956                1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3957     tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3958 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3959
3960     table_counter += 1
3961     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(flv_check = Gtk::CheckButton.new(utf8(_("Use embedded flash player for videos,\nand use this .flv generator:")))),
3962                0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3963     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(flv_generator_entry = Gtk::Entry.new.set_text($config['flv-generator']).set_sensitive(false)),
3964                1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3965     tooltips.set_tip(flv_check, utf8(_("Flowplayer will be used for embedded video playback")), nil)
3966     tooltips.set_tip(flv_generator_entry, utf8(_("Use %f to specify the input filename, %o the output filename;
3967 for example: /usr/bin/ffmpeg -i %f -b ${i}k -ar 22050 -ab 32k %o")), nil)
3968
3969     table_counter += 1
3970     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3971                0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3972     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(smp_hbox = Gtk::HBox.new.add(smp_spin = Gtk::SpinButton.new(2, 16, 1)).add(Gtk::Label.new(utf8(_("processors")))).set_sensitive(false)),
3973                1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3974     tooltips.set_tip(smp_check, utf8(_("When activated, this option allows the thumbnails creation to run faster. However, if you don't have a multi-processor machine, this will only slow down processing!")), nil)
3975
3976     table_counter += 1
3977     tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3978                0, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3979     tooltips.set_tip(nogestures_check, utf8(_("Mouse gestures are 'unusual' mouse movements triggering special actions, and are great for speeding up your editions. Get details on available mouse gestures from the Help menu.")), nil)
3980
3981     table_counter += 1
3982     tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original photos/videos as well"))),
3983                0, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3984     tooltips.set_tip(deleteondisk_check, utf8(_("Normally, deleting a photo or video in booh only removes it from the web-album. If you check this option, the original file in source directory will be removed as well. Undo is possible, since actual deletion is performed only when web-album is saved.")), nil)
3985
3986     flv_check.signal_connect('toggled') {
3987         flv_generator_entry.sensitive = flv_check.active?
3988     }
3989     if $config['use-flv'] == 'true'
3990         flv_check.active = true
3991     end
3992     smp_check.signal_connect('toggled') {
3993         smp_hbox.sensitive = smp_check.active?
3994     }
3995     if $config['mproc']
3996         smp_check.active = true
3997         smp_spin.value = $config['mproc'].to_i
3998     end
3999     nogestures_check.active = $config['nogestures']
4000     deleteondisk_check.active = $config['deleteondisk']
4001
4002     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
4003     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
4004                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
4005     tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
4006                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
4007     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nphotos in new albums:"))),
4008                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
4009     tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
4010                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
4011     tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
4012                2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
4013     tooltips.set_tip(commentsformat_entry, utf8(_("Normally, filenames without extension are used as comments for photos and videos in new albums. Use this entry to use something else.")), nil)
4014     commentsformat_help.signal_connect('clicked') {
4015         show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
4016 hence you should look at ImageMagick/identify documentation for the most    
4017 accurate and up-to-date documentation. Last time I checked, documentation
4018 was:
4019
4020 Print information about the image in a format of your choosing. You can
4021 include the image filename, type, width, height, Exif data, or other image
4022 attributes by embedding special format characters:                          
4023
4024                      %O   page offset
4025                      %P   page width and height                             
4026                      %b   file size                                         
4027                      %c   comment                                           
4028                      %d   directory                                         
4029                      %e   filename extension                                
4030                      %f   filename                                          
4031                      %g   page geometry                                     
4032                      %h   height                                            
4033                      %i   input filename                                    
4034                      %k   number of unique colors                           
4035                      %l   label                                             
4036                      %m   magick                                            
4037                      %n   number of scenes                                  
4038                      %o   output filename                                   
4039                      %p   page number                                       
4040                      %q   quantum depth                                     
4041                      %r   image class and colorspace                        
4042                      %s   scene number                                      
4043                      %t   top of filename                                   
4044                      %u   unique temporary filename                         
4045                      %w   width                                             
4046                      %x   x resolution