*** empty log message ***
[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         tmp = Tempfile.new("boohtemp")
2905         tmp.close!
2906         #- for security
2907         ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2908         ios.close
2909         $tempfiles << $filename << "#{$filename}.backup"
2910     else
2911         $orig_filename = nil
2912     end
2913     return result
2914 end
2915
2916 def open_file_popup
2917     if !ask_save_modifications(utf8(_("Save this album?")),
2918                                utf8(_("Do you want to save the changes to this album?")),
2919                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2920         return
2921     end
2922     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2923                                     nil,
2924                                     Gtk::FileChooser::ACTION_OPEN,
2925                                     nil,
2926                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2927     fc.add_shortcut_folder(File.expand_path("~/.booh"))
2928     fc.set_current_folder(File.expand_path("~/.booh"))
2929     fc.transient_for = $main_window
2930     ok = false
2931     while !ok
2932         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2933             push_mousecursor_wait(fc)
2934             msg = open_file_user(fc.filename)
2935             pop_mousecursor(fc)
2936             if msg
2937                 show_popup(fc, msg)
2938                 ok = false
2939             else
2940                 ok = true
2941             end
2942         else
2943             ok = true
2944         end
2945     end
2946     fc.destroy
2947 end
2948
2949 def additional_booh_options
2950     options = ''
2951     if $config['mproc']
2952         options += "--mproc #{$config['mproc'].to_i} "
2953     end
2954     options += "--comments-format '#{$config['comments-format']}' "
2955     if $config['transcode-videos']
2956         options += "--transcode-videos '#{$config['transcode-videos']}' "
2957     end
2958     return options
2959 end
2960
2961 def ask_multi_languages(value)
2962     if ! value.nil?
2963         spl = value.split(',')
2964         value = [ spl[0..-2], spl[-1] ]
2965     end
2966
2967     dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
2968                              $main_window,
2969                              Gtk::Dialog::MODAL,
2970                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2971                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2972
2973     lbl = Gtk::Label.new
2974     lbl.markup = utf8(
2975 _("You can choose to activate <b>multi-languages</b> support for this web-album
2976 (it will work only if you publish your web-album on an Apache web-server). This will
2977 use the MultiViews feature of Apache; the pages will be served according to the
2978 value of the Accept-Language HTTP header sent by the web browsers, so that people
2979 with different languages preferences will be able to browse your web-album with
2980 navigation in their language (if language is available).
2981 "))
2982
2983     dialog.vbox.add(lbl)
2984     dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
2985                                                                          add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
2986                                                                                                      add(languages = Gtk::Button.new))))
2987
2988     pick_languages = proc {
2989         dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
2990                                   $main_window,
2991                                   Gtk::Dialog::MODAL,
2992                                   [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2993                                   [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2994
2995         dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
2996         hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
2997         cbs = []
2998         SUPPORTED_LANGUAGES.each { |lang|
2999             hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3000             if ! value.nil? && value[0].include?(lang)
3001                 cb.active = true
3002             end
3003             cbs << [ lang, cb ]
3004         }
3005
3006         dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3007         hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3008         fallback_language = nil
3009         hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3010         fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3011         if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3012             fbl_rb.active = true
3013             fallback_language = SUPPORTED_LANGUAGES[0]
3014         end
3015         SUPPORTED_LANGUAGES[1..-1].each { |lang|
3016             hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3017             rb.signal_connect('clicked') { fallback_language = lang }
3018             if ! value.nil? && value[1] == lang
3019                 rb.active = true
3020             end
3021         }
3022
3023         dialog2.window_position = Gtk::Window::POS_MOUSE
3024         dialog2.show_all
3025
3026         resp = nil
3027         dialog2.run { |response|
3028             resp = response
3029             if resp == Gtk::Dialog::RESPONSE_OK
3030                 value = []
3031                 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3032                 value[1] = fallback_language
3033                 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3034             end
3035             dialog2.destroy
3036         }
3037         resp
3038     }
3039
3040     languages.signal_connect('clicked') {
3041         pick_languages.call
3042     }
3043     dialog.window_position = Gtk::Window::POS_MOUSE
3044     if value.nil?
3045         rb_no.active = true
3046     else
3047         rb_yes.active = true
3048         languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3049     end
3050     rb_no.signal_connect('clicked') {
3051         if rb_no.active?
3052             languages.hide
3053         else
3054             if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3055                 rb_no.activate
3056             else
3057                 languages.show
3058             end
3059         end
3060     }
3061     oldval = value
3062     dialog.show_all
3063     if rb_no.active?
3064         languages.hide
3065     end
3066
3067     dialog.run { |response|
3068         if rb_no.active?
3069             value = nil
3070         end
3071         dialog.destroy
3072         if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3073             if value.nil?
3074                 return [ true, nil ]
3075             else
3076                 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3077             end
3078         else
3079             return [ false ]
3080         end
3081     }
3082 end
3083
3084 def new_album
3085     if !ask_save_modifications(utf8(_("Save this album?")),
3086                                utf8(_("Do you want to save the changes to this album?")),
3087                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3088         return
3089     end
3090     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3091                              $main_window,
3092                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3093                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3094                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3095     
3096     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3097     tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3098                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3099     tbl.attach(src = Gtk::Entry.new,
3100                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3101     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3102                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3103     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3104                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3105     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3106                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3107     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3108                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3109     tbl.attach(dest = Gtk::Entry.new,
3110                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3111     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3112                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3113     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3114                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3115     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3116                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3117     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3118                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3119
3120     tooltips = Gtk::Tooltips.new
3121     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3122     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3123                          pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3124     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3125                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3126     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3127     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)
3128     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3129                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3130     nperpage_model = Gtk::ListStore.new(String, String)
3131     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3132                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3133     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3134     nperpagecombo.set_attributes(crt, { :markup => 0 })
3135     iter = nperpage_model.append
3136     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3137     iter[1] = nil
3138     [ 12, 20, 30, 40, 50 ].each { |v|
3139         iter = nperpage_model.append
3140         iter[0] = iter[1] = v.to_s
3141     }
3142     nperpagecombo.active = 0
3143
3144     multilanguages_value = nil
3145     vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3146                                         pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3147     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)
3148     multilanguages.signal_connect('clicked') {
3149         retval = ask_multi_languages(multilanguages_value)
3150         if retval[0] 
3151             multilanguages_value = retval[1]
3152         end
3153         if multilanguages_value
3154             ml_label.text = utf8(_("Multi-languages: enabled."))
3155         else
3156             ml_label.text = utf8(_("Multi-languages: disabled."))
3157         end
3158     }
3159     if $config['default-multi-languages']
3160         multilanguages_value = $config['default-multi-languages']
3161         ml_label.text = utf8(_("Multi-languages: enabled."))
3162     else
3163         ml_label.text = utf8(_("Multi-languages: disabled."))
3164     end
3165
3166     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3167                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3168     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)
3169     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3170                                    pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3171     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)
3172
3173     src_nb_calculated_for = ''
3174     src_nb_thread = nil
3175     process_src_nb = proc {
3176         if src.text != src_nb_calculated_for
3177             src_nb_calculated_for = src.text
3178             if src_nb_thread
3179                 Thread.kill(src_nb_thread)
3180                 src_nb_thread = nil
3181             end
3182             if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3183                 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3184             else
3185                 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3186                     if File.readable?(from_utf8_safe(src_nb_calculated_for))
3187                         src_nb_thread = Thread.new {
3188                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3189                             total = { 'image' => 0, 'video' => 0, nil => 0 }
3190                             `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3191                                 if File.basename(dir) =~ /^\./
3192                                     next
3193                                 else
3194                                     begin
3195                                         Dir.entries(dir.chomp).each { |file|
3196                                             total[entry2type(file)] += 1
3197                                         }
3198                                     rescue Errno::EACCES, Errno::ENOENT
3199                                     end
3200                                 end
3201                             }
3202                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3203                             src_nb_thread = nil
3204                         }
3205                     else
3206                         src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3207                     end
3208                 else
3209                     src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3210                 end
3211             end
3212         end
3213         true
3214     }
3215     timeout_src_nb = Gtk.timeout_add(100) {
3216         process_src_nb.call
3217     }
3218
3219     src_browse.signal_connect('clicked') {
3220         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3221                                         nil,
3222                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
3223                                         nil,
3224                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3225         fc.transient_for = $main_window
3226         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3227             src.text = utf8(fc.filename)
3228             process_src_nb.call
3229             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3230         end
3231         fc.destroy
3232     }
3233
3234     dest_browse.signal_connect('clicked') {
3235         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3236                                         nil,
3237                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
3238                                         nil,
3239                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3240         fc.transient_for = $main_window
3241         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3242             dest.text = utf8(fc.filename)
3243         end
3244         fc.destroy
3245     }
3246
3247     conf_browse.signal_connect('clicked') {
3248         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3249                                         nil,
3250                                         Gtk::FileChooser::ACTION_SAVE,
3251                                         nil,
3252                                         [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3253         fc.transient_for = $main_window
3254         fc.add_shortcut_folder(File.expand_path("~/.booh"))
3255         fc.set_current_folder(File.expand_path("~/.booh"))
3256         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3257             conf.text = utf8(fc.filename)
3258         end
3259         fc.destroy
3260     }
3261
3262     theme_sizes = []
3263     nperrows = []
3264     recreate_theme_config = proc {
3265         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3266         theme_sizes = []
3267         select_theme(theme_button.label, 'all', optimize432.active?, nil)
3268         $images_size.each { |s|
3269             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3270             if !s['optional']
3271                 cb.active = true
3272             end
3273             tooltips.set_tip(cb, utf8(s['description']), nil)
3274             theme_sizes << { :widget => cb, :value => s['name'] }
3275         }
3276         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3277         tooltips = Gtk::Tooltips.new
3278         tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3279         theme_sizes << { :widget => cb, :value => 'original' }
3280         sizes.show_all
3281
3282         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3283         nperrow_group = nil
3284         nperrows = []
3285         $allowed_N_values.each { |n|
3286             if nperrow_group
3287                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3288             else
3289                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3290             end
3291             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3292             if $default_N == n
3293                 rb.active = true
3294             end
3295             nperrows << { :widget => rb, :value => n }
3296         }
3297         nperrowradios.show_all
3298     }
3299     recreate_theme_config.call
3300
3301     theme_button.signal_connect('clicked') {
3302         if newtheme = theme_choose(theme_button.label)
3303             theme_button.label = newtheme
3304             recreate_theme_config.call
3305         end
3306     }
3307
3308     dialog.vbox.add(frame1)
3309     dialog.vbox.add(frame2)
3310     dialog.show_all
3311
3312     keepon = true
3313     ok = true
3314     while keepon
3315         dialog.run { |response|
3316             if response == Gtk::Dialog::RESPONSE_OK
3317                 srcdir = from_utf8_safe(src.text)
3318                 destdir = from_utf8_safe(dest.text)
3319                 confpath = from_utf8_safe(conf.text)
3320                 if src.text != '' && srcdir == ''
3321                     show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3322                     src.grab_focus
3323                 elsif !File.directory?(srcdir)
3324                     show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3325                     src.grab_focus
3326                 elsif dest.text != '' && destdir == ''
3327                     show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3328                     dest.grab_focus
3329                 elsif destdir != make_dest_filename(destdir)
3330                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3331                     dest.grab_focus
3332                 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3333                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3334 inside it will be permanently removed before creating the web-album!
3335 Are you sure you want to continue?")), { :okcancel => true })
3336                     dest.grab_focus
3337                 elsif File.exists?(destdir) && !File.directory?(destdir)
3338                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3339                     dest.grab_focus
3340                 elsif conf.text == ''
3341                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3342                     conf.grab_focus
3343                 elsif conf.text != '' && confpath == ''
3344                     show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3345                     conf.grab_focus
3346                 elsif File.directory?(confpath)
3347                     show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3348                     conf.grab_focus
3349                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3350                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3351                 else
3352                     system("mkdir '#{destdir}'")
3353                     if !File.directory?(destdir)
3354                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3355                         dest.grab_focus
3356                     else
3357                         keepon = false
3358                     end
3359                 end
3360             else
3361                 keepon = ok = false
3362             end
3363         }
3364     end
3365     if ok
3366         srcdir = from_utf8(src.text)
3367         destdir = from_utf8(dest.text)
3368         configskel = File.expand_path(from_utf8(conf.text))
3369         theme = theme_button.label
3370         #- some sort of automatic theme preference
3371         $config['default-theme'] = theme
3372         $config['default-multi-languages'] = multilanguages_value
3373         $config['default-optimize32'] = optimize432.active?.to_s
3374         sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3375         nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3376         nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3377         opt432 = optimize432.active?
3378         madewith = madewithentry.text.gsub('\'', '&#39;')  #- because the parameters to booh-backend are between apostrophes
3379         indexlink = indexlinkentry.text.gsub('\'', '&#39;')
3380     end
3381     if src_nb_thread
3382         Thread.kill(src_nb_thread)
3383         gtk_thread_flush  #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3384     end
3385     dialog.destroy
3386     Gtk.timeout_remove(timeout_src_nb)
3387
3388     if ok
3389         call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3390                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3391                      (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3392                      (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
3393                      "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3394                      utf8(_("Please wait while scanning source directory...")),
3395                      'full scan',
3396                      { :closure_after => proc {
3397                              open_file_user(configskel)
3398                              $main_window.urgency_hint = true
3399                          } })
3400     end
3401 end
3402
3403 def properties
3404     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3405                              $main_window,
3406                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3407                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3408                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3409     
3410     source = $xmldoc.root.attributes['source']
3411     dest = $xmldoc.root.attributes['destination']
3412     theme = $xmldoc.root.attributes['theme']
3413     opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3414     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3415     nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3416     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3417     if limit_sizes
3418         limit_sizes = limit_sizes.split(/,/)
3419     end
3420     madewith = ($xmldoc.root.attributes['made-with'] || '').gsub('&#39;', '\'')
3421     indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub('&#39;', '\'')
3422     save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3423
3424     tooltips = Gtk::Tooltips.new
3425     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3426     tbl.attach(Gtk::Label.new(utf8(_("Directory of source photos/videos: "))),
3427                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3428     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3429                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3430     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3431                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3432     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3433                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3434     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3435                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3436     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3437                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3438
3439     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3440     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3441                                    pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3442     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3443                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3444     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3445     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)
3446     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3447                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3448     nperpage_model = Gtk::ListStore.new(String, String)
3449     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3450                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3451     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3452     nperpagecombo.set_attributes(crt, { :markup => 0 })
3453     iter = nperpage_model.append
3454     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3455     iter[1] = nil
3456     [ 12, 20, 30, 40, 50 ].each { |v|
3457         iter = nperpage_model.append
3458         iter[0] = iter[1] = v.to_s
3459         if nperpage && nperpage == v.to_s
3460             nperpagecombo.active_iter = iter
3461         end
3462     }
3463     if nperpagecombo.active_iter.nil?
3464         nperpagecombo.active = 0
3465     end
3466
3467     vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3468                                         pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3469     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)
3470     ml_update = proc {
3471         if save_multilanguages_value
3472             ml_label.text = utf8(_("Multi-languages: enabled."))
3473         else
3474             ml_label.text = utf8(_("Multi-languages: disabled."))
3475         end
3476     }
3477     ml_update.call
3478     multilanguages.signal_connect('clicked') {
3479         retval = ask_multi_languages(save_multilanguages_value)
3480         if retval[0] 
3481             save_multilanguages_value = retval[1]
3482         end
3483         ml_update.call
3484     }
3485
3486     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3487                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3488     if indexlink
3489         indexlinkentry.text = indexlink
3490     end
3491     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)
3492     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3493                                    pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3494     if madewith
3495         madewithentry.text = madewith
3496     end
3497     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)
3498
3499     theme_sizes = []
3500     nperrows = []
3501     recreate_theme_config = proc {
3502         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3503         theme_sizes = []
3504         select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3505
3506         $images_size.each { |s|
3507             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))