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