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