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