eebf210c5d303b031b12ba80c285b89021658f14
[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)
1996         else
1997             set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
1998         end
1999     }
2000
2001     infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2002     refresh_thread = Thread.new {
2003         directories_counter = 0
2004         while line = infopipe.gets
2005             if line =~ /^directories: (\d+), sizes: (\d+)/
2006                 directories = $1.to_f + 1
2007                 sizes = $2.to_f
2008             elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2009                 elements = $3.to_f + 1
2010                 if mode == 'web-album'
2011                     elements += sizes
2012                 end
2013                 element_counter = 0
2014                 gtk_thread_protect { pb1_1.fraction = 0 }
2015                 if mode != 'one dir scan'
2016                     newtext = utf8(full_src_dir_to_rel($1, $2))
2017                     newtext = '/' if newtext == ''
2018                     gtk_thread_protect { pb1_2.text = newtext }
2019                     directories_counter += 1
2020                     gtk_thread_protect {
2021                         pb1_2.fraction = directories_counter / directories
2022                         update_progression_title_pb1.call
2023                     }
2024                 end
2025             elsif line =~ /^processing element$/
2026                 element_counter += 1
2027                 gtk_thread_protect {
2028                     pb1_1.fraction = element_counter / elements
2029                     update_progression_title_pb1.call
2030                 }
2031             elsif line =~ /^processing size$/
2032                 element_counter += 1
2033                 gtk_thread_protect {
2034                     pb1_1.fraction = element_counter / elements
2035                     update_progression_title_pb1.call
2036                 }
2037             elsif line =~ /^finished processing sizes$/
2038                 gtk_thread_protect { pb1_1.fraction = 1 }
2039             elsif line =~ /^creating index.html$/
2040                 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2041                 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2042                 directories_counter = 0
2043             elsif line =~ /^index.html: (.+)\|(.+)/
2044                 newtext = utf8(full_src_dir_to_rel($1, $2))
2045                 newtext = '/' if newtext == ''
2046                 gtk_thread_protect { pb2.text = newtext }
2047                 directories_counter += 1
2048                 gtk_thread_protect {
2049                     pb2.fraction = directories_counter / directories
2050                     set_mainwindow_title(0.9 + pb2.fraction / 10)
2051                 }
2052             elsif line =~ /^die: (.*)$/
2053                 $diemsg = $1
2054             end
2055         end
2056     }
2057
2058     w.add(vb)
2059     w.signal_connect('delete-event') { w.destroy }
2060     w.signal_connect('destroy') {
2061         Thread.kill(refresh_thread)
2062         gtk_thread_flush  #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2063         if infopipe_path
2064             infopipe.close
2065             File.delete(infopipe_path)
2066         end
2067         set_mainwindow_title(nil)
2068     }
2069     w.window_position = Gtk::Window::POS_CENTER
2070     w.show_all
2071
2072     return [ b, w ]
2073 end
2074
2075 def call_backend(cmd, waitmsg, mode, params)
2076     pipe = Tempfile.new("boohpipe")
2077     pipe.close!
2078     system("mkfifo #{pipe.path}")
2079     cmd += " --info-pipe #{pipe.path}"
2080     button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2081     pid = nil
2082     Thread.new {
2083         msg 2, cmd
2084         if pid = fork
2085             id, exitstatus = Process.waitpid2(pid)
2086             gtk_thread_protect { w8.destroy }
2087             if exitstatus == 0
2088                 if params[:successmsg]
2089                     gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2090                 end
2091                 if params[:closure_after]
2092                     gtk_thread_protect(&params[:closure_after])
2093                 end
2094             elsif exitstatus == 15
2095                 #- say nothing, user aborted
2096             else
2097                 gtk_thread_protect { show_popup($main_window,
2098                                                 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2099             end
2100         else
2101             exec(cmd)
2102         end
2103     }
2104     button.signal_connect('clicked') {
2105         Process.kill('SIGTERM', pid)
2106     }
2107 end
2108
2109 def save_changes(*forced)
2110     if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2111         return
2112     end
2113
2114     $xmldir.delete_attribute('already-generated')
2115
2116     propagate_children = proc { |xmldir|
2117         if xmldir.attributes['subdirs-caption']
2118             xmldir.delete_attribute('already-generated')
2119         end
2120         xmldir.elements.each('dir') { |element|
2121             propagate_children.call(element)
2122         }
2123     }
2124
2125     if $xmldir.child_byname_notattr('dir', 'deleted')
2126         new_title = $subalbums_title.buffer.text
2127         if new_title != $xmldir.attributes['subdirs-caption']
2128             parent = $xmldir.parent
2129             if parent.name == 'dir'
2130                 parent.delete_attribute('already-generated')
2131             end
2132             propagate_children.call($xmldir)
2133         end
2134         $xmldir.add_attribute('subdirs-caption', new_title)
2135         $xmldir.elements.each('dir') { |element|
2136             if !element.attributes['deleted']
2137                 path = element.attributes['path']
2138                 newtext = $subalbums_edits[path][:editzone].buffer.text
2139                 if element.attributes['subdirs-caption']
2140                     if element.attributes['subdirs-caption'] != newtext
2141                         propagate_children.call(element)
2142                     end
2143                     element.add_attribute('subdirs-caption',     newtext)
2144                     element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2145                 else
2146                     if element.attributes['thumbnails-caption'] != newtext
2147                         element.delete_attribute('already-generated')
2148                     end
2149                     element.add_attribute('thumbnails-caption',     newtext)
2150                     element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2151                 end
2152             end
2153         }
2154     end
2155
2156     if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2157         if $xmldir.attributes['thumbnails-caption']
2158             path = $xmldir.attributes['path']
2159             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2160         end
2161     elsif $xmldir.attributes['thumbnails-caption']
2162         $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2163     end
2164
2165     if $xmldir.attributes['thumbnails-caption']
2166         if edit = $subalbums_edits[$xmldir.attributes['path']]
2167             $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2168         end
2169     end
2170
2171     #- remove and reinsert elements to reflect new ordering
2172     saves = {}
2173     cpt = 0
2174     $xmldir.elements.each { |element|
2175         if element.name == 'image' || element.name == 'video'
2176             saves[element.attributes['filename']] = element.remove
2177             cpt += 1
2178         end
2179     }
2180     $autotable.current_order.each { |path|
2181         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2182         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2183         saves.delete(path)
2184     }
2185     saves.each_key { |path|
2186         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2187         chld.add_attribute('deleted', 'true')
2188     }
2189 end
2190
2191 def sort_by_exif_date
2192     $modified = true
2193     save_changes
2194     current_order = []
2195     $xmldir.elements.each { |element|
2196         if element.name == 'image' || element.name == 'video'
2197             current_order << element.attributes['filename']
2198         end
2199     }
2200
2201     #- look for EXIF dates
2202     dates = {}
2203
2204     if current_order.size > 20
2205         w = create_window
2206         w.set_transient_for($main_window)
2207         w.modal = true
2208         vb = Gtk::VBox.new(false, 5).set_border_width(5)
2209         vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2210         vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2211         bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2212         b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2213         vb.pack_end(bottom, false, false)
2214         w.add(vb)
2215         w.signal_connect('delete-event') { w.destroy }
2216         w.window_position = Gtk::Window::POS_CENTER
2217         w.show_all
2218
2219         aborted = false
2220         b.signal_connect('clicked') { aborted = true }
2221         i = 0
2222         current_order.each { |f|
2223             i += 1
2224             if entry2type(f) == 'image'
2225                 pb.text = f
2226                 pb.fraction = i.to_f / current_order.size
2227                 Gtk.main_iteration while Gtk.events_pending?
2228                 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2229                 if ! date_time.nil?
2230                     dates[f] = date_time
2231                 end
2232             end
2233             if aborted
2234                 break
2235             end
2236         }
2237         w.destroy
2238         if aborted
2239             return
2240         end
2241
2242     else
2243         current_order.each { |f|
2244             date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2245             if ! date_time.nil?
2246                 dates[f] = date_time
2247             end
2248         }
2249     end
2250
2251     saves = {}
2252     $xmldir.elements.each { |element|
2253         if element.name == 'image' || element.name == 'video'
2254             saves[element.attributes['filename']] = element.remove
2255         end
2256     }
2257
2258     neworder = smartsort(current_order, dates)
2259
2260     neworder.each { |f|
2261         $xmldir.add_element(saves[f].name, saves[f].attributes)
2262     }
2263
2264     #- let the auto-table reflect new ordering
2265     change_dir
2266 end
2267
2268 def remove_all_captions
2269     $modified = true
2270     texts = {}
2271     $autotable.current_order.each { |path|
2272         texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2273         $name2widgets[File.basename(path)][:textview].buffer.text = ''
2274     }
2275     save_undo(_("remove all captions"),
2276               proc { |texts|
2277                   texts.each_key { |key|
2278                       $name2widgets[key][:textview].buffer.text = texts[key]
2279                   }
2280                   $notebook.set_page(1)
2281                   proc {
2282                       texts.each_key { |key|
2283                           $name2widgets[key][:textview].buffer.text = ''
2284                       }
2285                       $notebook.set_page(1)
2286                   }
2287               }, texts)
2288 end
2289
2290 def change_dir
2291     $selected_elements.each_key { |path|
2292         $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2293     }
2294     $autotable.clear
2295     $vbox2widgets = {}
2296     $name2widgets = {}
2297     $name2closures = {}
2298     $selected_elements = {}
2299     $cuts = []
2300     $multiple_dnd = []
2301     UndoHandler.cleanup
2302     $undo_tb.sensitive = $undo_mb.sensitive = false
2303     $redo_tb.sensitive = $redo_mb.sensitive = false
2304
2305     if !$current_path
2306         return
2307     end
2308
2309     $subalbums_vb.children.each { |chld|
2310         $subalbums_vb.remove(chld)
2311     }
2312     $subalbums = Gtk::Table.new(0, 0, true)
2313     current_y_sub_albums = 0
2314
2315     $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"])
2316     $subalbums_edits = {}
2317     subalbums_counter = 0
2318     subalbums_edits_bypos = {}
2319
2320     add_subalbum = proc { |xmldir, counter|
2321         $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2322         subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2323         if xmldir == $xmldir
2324             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2325             captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2326             caption = xmldir.attributes['thumbnails-caption']
2327             infotype = 'thumbnails'
2328         else
2329             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2330             captionfile, caption = find_subalbum_caption_info(xmldir)
2331             infotype = find_subalbum_info_type(xmldir)
2332         end
2333         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2334         hbox = Gtk::HBox.new
2335         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2336         f = Gtk::Frame.new
2337         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2338
2339         img = nil
2340         my_gen_real_thumbnail = proc {
2341             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2342         }
2343
2344         if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2345             f.add(img = Gtk::Image.new)
2346             my_gen_real_thumbnail.call
2347         else
2348             f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2349         end
2350         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2351         $subalbums.attach(hbox,
2352                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2353
2354         frame, textview = create_editzone($subalbums_sw, 0, img)
2355         textview.buffer.text = caption
2356         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2357                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2358
2359         change_image = proc {
2360             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2361                                             nil,
2362                                             Gtk::FileChooser::ACTION_OPEN,
2363                                             nil,
2364                                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2365             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2366             fc.transient_for = $main_window
2367             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))
2368             f.add(preview_img = Gtk::Image.new)
2369             preview.show_all
2370             fc.signal_connect('update-preview') { |w|
2371                 begin
2372                     if fc.preview_filename
2373                         preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2374                         fc.preview_widget_active = true
2375                     end
2376                 rescue Gdk::PixbufError
2377                     fc.preview_widget_active = false
2378                 end
2379             }
2380             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2381                 $modified = true
2382                 old_file = captionfile
2383                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2384                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2385                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2386                 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2387
2388                 new_file = fc.filename
2389                 msg 3, "new captionfile is: #{fc.filename}"
2390                 perform_changefile = proc {
2391                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2392                     $modified_pixbufs.delete(thumbnail_file)
2393                     xmldir.delete_attribute("#{infotype}-rotate")
2394                     xmldir.delete_attribute("#{infotype}-color-swap")
2395                     xmldir.delete_attribute("#{infotype}-enhance")
2396                     xmldir.delete_attribute("#{infotype}-seektime")
2397                     my_gen_real_thumbnail.call
2398                 }
2399                 perform_changefile.call
2400
2401                 save_undo(_("change caption file for sub-album"),
2402                           proc {
2403                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2404                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2405                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2406                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2407                               xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2408                               my_gen_real_thumbnail.call
2409                               $notebook.set_page(0)
2410                               proc {
2411                                   perform_changefile.call
2412                                   $notebook.set_page(0)
2413                               }
2414                           })
2415             end
2416             fc.destroy
2417         }
2418
2419         refresh = proc {
2420             if File.exists?(thumbnail_file)
2421                 File.delete(thumbnail_file)
2422             end
2423             my_gen_real_thumbnail.call
2424         }
2425
2426         rotate_and_cleanup = proc { |angle|
2427             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2428             if File.exists?(thumbnail_file)
2429                 File.delete(thumbnail_file)
2430             end
2431         }
2432
2433         move = proc { |direction|
2434             $modified = true
2435
2436             save_changes('forced')
2437             oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2438             if direction == 'up'
2439                 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2440                 subalbums_edits_bypos[oldpos - 1][:position] += 1
2441             end
2442             if direction == 'down'
2443                 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2444                 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2445             end
2446             if direction == 'top'
2447                 for i in 1 .. oldpos - 1
2448                     subalbums_edits_bypos[i][:position] += 1
2449                 end
2450                 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2451             end
2452             if direction == 'bottom'
2453                 for i in oldpos + 1 .. subalbums_counter
2454                     subalbums_edits_bypos[i][:position] -= 1
2455                 end
2456                 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2457             end
2458
2459             elems = []
2460             $xmldir.elements.each('dir') { |element|
2461                 if (!element.attributes['deleted'])
2462                     elems << [ element.attributes['path'], element.remove ]
2463                 end
2464             }
2465             elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2466                   each { |e| $xmldir.add_element(e[1]) }
2467             #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2468             $xmldir.elements.each('descendant::dir') { |elem|
2469                 elem.delete_attribute('already-generated')
2470             }
2471
2472             sel = $albums_tv.selection.selected_rows
2473             change_dir
2474             populate_subalbums_treeview(false)
2475             $albums_tv.selection.select_path(sel[0])
2476         }
2477
2478         color_swap_and_cleanup = proc {
2479             perform_color_swap_and_cleanup = proc {
2480                 color_swap(xmldir, "#{infotype}-")
2481                 my_gen_real_thumbnail.call
2482             }
2483             perform_color_swap_and_cleanup.call
2484
2485             save_undo(_("color swap"),
2486                       proc {
2487                           perform_color_swap_and_cleanup.call
2488                           $notebook.set_page(0)
2489                           proc {
2490                               perform_color_swap_and_cleanup.call
2491                               $notebook.set_page(0)
2492                           }
2493                       })
2494         }
2495
2496         change_seektime_and_cleanup = proc {
2497             if values = ask_new_seektime(xmldir, "#{infotype}-")
2498                 perform_change_seektime_and_cleanup = proc { |val|
2499                     change_seektime(xmldir, "#{infotype}-", val)
2500                     my_gen_real_thumbnail.call
2501                 }
2502                 perform_change_seektime_and_cleanup.call(values[:new])
2503
2504                 save_undo(_("specify seektime"),
2505                           proc {
2506                               perform_change_seektime_and_cleanup.call(values[:old])
2507                               $notebook.set_page(0)
2508                               proc {
2509                                   perform_change_seektime_and_cleanup.call(values[:new])
2510                                   $notebook.set_page(0)
2511                               }
2512                           })
2513             end
2514         }
2515
2516         whitebalance_and_cleanup = proc {
2517             if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2518                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2519                 perform_change_whitebalance_and_cleanup = proc { |val|
2520                     change_whitebalance(xmldir, "#{infotype}-", val)
2521                     recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2522                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2523                     if File.exists?(thumbnail_file)
2524                         File.delete(thumbnail_file)
2525                     end
2526                 }
2527                 perform_change_whitebalance_and_cleanup.call(values[:new])
2528                 
2529                 save_undo(_("fix white balance"),
2530                           proc {
2531                               perform_change_whitebalance_and_cleanup.call(values[:old])
2532                               $notebook.set_page(0)
2533                               proc {
2534                                   perform_change_whitebalance_and_cleanup.call(values[:new])
2535                                   $notebook.set_page(0)
2536                               }
2537                           })
2538             end
2539         }
2540
2541         gammacorrect_and_cleanup = proc {
2542             if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2543                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2544                 perform_change_gammacorrect_and_cleanup = proc { |val|
2545                     change_gammacorrect(xmldir, "#{infotype}-", val)
2546                     recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2547                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2548                     if File.exists?(thumbnail_file)
2549                         File.delete(thumbnail_file)
2550                     end
2551                 }
2552                 perform_change_gammacorrect_and_cleanup.call(values[:new])
2553                 
2554                 save_undo(_("gamma correction"),
2555                           proc {
2556                               perform_change_gammacorrect_and_cleanup.call(values[:old])
2557                               $notebook.set_page(0)
2558                               proc {
2559                                   perform_change_gammacorrect_and_cleanup.call(values[:new])
2560                                   $notebook.set_page(0)
2561                               }
2562                           })
2563             end
2564         }
2565
2566         enhance_and_cleanup = proc {
2567             perform_enhance_and_cleanup = proc {
2568                 enhance(xmldir, "#{infotype}-")
2569                 my_gen_real_thumbnail.call
2570             }
2571             
2572             perform_enhance_and_cleanup.call
2573             
2574             save_undo(_("enhance"),
2575                       proc {
2576                           perform_enhance_and_cleanup.call
2577                           $notebook.set_page(0)
2578                           proc {
2579                               perform_enhance_and_cleanup.call
2580                               $notebook.set_page(0)
2581                           }
2582                       })
2583         }
2584
2585         evtbox.signal_connect('button-press-event') { |w, event|
2586             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2587                 if $r90.active?
2588                     rotate_and_cleanup.call(90)
2589                 elsif $r270.active?
2590                     rotate_and_cleanup.call(-90)
2591                 elsif $enhance.active?
2592                     enhance_and_cleanup.call
2593                 end
2594             end
2595             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2596                 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2597                                      { :forbid_left => true, :forbid_right => true,
2598                                        :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2599                                        :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2600                                      { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2601                                        :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2602                                        :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2603             end
2604             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2605                 change_image.call
2606                 true   #- handled
2607             end
2608         }
2609         evtbox.signal_connect('button-press-event') { |w, event|
2610             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2611             false
2612         }
2613
2614         evtbox.signal_connect('button-release-event') { |w, event|
2615             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2616                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2617                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2618                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2619                     msg 3, "gesture rotate: #{angle}"
2620                     rotate_and_cleanup.call(angle)
2621                 end
2622             end
2623             $gesture_press = nil
2624         }
2625                 
2626         $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2627         $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2628         current_y_sub_albums += 1
2629     }
2630
2631     if $xmldir.child_byname_notattr('dir', 'deleted')
2632         #- title edition
2633         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2634         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2635         $subalbums_title.set_justification(Gtk::Justification::CENTER)
2636         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2637         #- this album image/caption
2638         if $xmldir.attributes['thumbnails-caption']
2639             add_subalbum.call($xmldir, 0)
2640         end
2641     end
2642     total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2643     $xmldir.elements.each { |element|
2644         if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2645             #- element (image or video) of this album
2646             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2647             msg 3, "dest_img: #{dest_img}"
2648             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2649             total[element.name] += 1
2650         end
2651         if element.name == 'dir' && !element.attributes['deleted']
2652             #- sub-album image/caption
2653             add_subalbum.call(element, subalbums_counter += 1)
2654             total[element.name] += 1
2655         end
2656     }
2657     $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2658                                                                                 total['image'], total['video'], total['dir'] ]))
2659     $subalbums_vb.add($subalbums)
2660     $subalbums_vb.show_all
2661
2662     if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2663         $notebook.get_tab_label($autotable_sw).sensitive = false
2664         $notebook.set_page(0)
2665         $thumbnails_title.buffer.text = ''
2666     else
2667         $notebook.get_tab_label($autotable_sw).sensitive = true
2668         $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2669     end
2670
2671     if !$xmldir.child_byname_notattr('dir', 'deleted')
2672         $notebook.get_tab_label($subalbums_sw).sensitive = false
2673         $notebook.set_page(1)
2674     else
2675         $notebook.get_tab_label($subalbums_sw).sensitive = true
2676     end
2677 end
2678
2679 def pixbuf_or_nil(filename)
2680     begin
2681         return Gdk::Pixbuf.new(filename)
2682     rescue
2683         return nil
2684     end
2685 end
2686
2687 def theme_choose(current)
2688     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2689                              $main_window,
2690                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2691                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2692                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2693
2694     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2695     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2696     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2697     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2698     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2699     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2700     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2701     treeview.signal_connect('button-press-event') { |w, event|
2702         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2703             dialog.response(Gtk::Dialog::RESPONSE_OK)
2704         end
2705     }
2706
2707     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2708
2709     `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2710         dir.chomp!
2711         iter = model.append
2712         iter[0] = File.basename(dir)
2713         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2714         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2715         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2716         if File.basename(dir) == current
2717             treeview.selection.select_iter(iter)
2718         end
2719     }
2720
2721     dialog.set_default_size(700, 400)
2722     dialog.vbox.show_all
2723     dialog.run { |response|
2724         iter = treeview.selection.selected
2725         dialog.destroy
2726         if response == Gtk::Dialog::RESPONSE_OK && iter
2727             return model.get_value(iter, 0)
2728         end
2729     }
2730     return nil
2731 end
2732
2733 def show_password_protections
2734     examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2735         child_iter = $albums_iters[xmldir.attributes['path']]
2736         if xmldir.attributes['password-protect']
2737             child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2738             already_protected = true
2739         elsif already_protected
2740             pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2741             if pix
2742                 pix = pix.saturate_and_pixelate(1, true)
2743             end
2744             child_iter[2] = pix
2745         else
2746             child_iter[2] = nil
2747         end
2748         xmldir.elements.each('dir') { |elem|
2749             if !elem.attributes['deleted']
2750                 examine_dir_elem.call(child_iter, elem, already_protected)
2751             end
2752         }
2753     }
2754     examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2755 end
2756
2757 def populate_subalbums_treeview(select_first)
2758     $albums_ts.clear
2759     $autotable.clear
2760     $albums_iters = {}
2761     $subalbums_vb.children.each { |chld|
2762         $subalbums_vb.remove(chld)
2763     }
2764
2765     source = $xmldoc.root.attributes['source']
2766     msg 3, "source: #{source}"
2767
2768     xmldir = $xmldoc.elements['//dir']
2769     if !xmldir || xmldir.attributes['path'] != source
2770         msg 1, _("Corrupted booh file...")
2771         return
2772     end
2773
2774     append_dir_elem = proc { |parent_iter, xmldir|
2775         child_iter = $albums_ts.append(parent_iter)
2776         child_iter[0] = File.basename(xmldir.attributes['path'])
2777         child_iter[1] = xmldir.attributes['path']
2778         $albums_iters[xmldir.attributes['path']] = child_iter
2779         msg 3, "puttin location: #{xmldir.attributes['path']}"
2780         xmldir.elements.each('dir') { |elem|
2781             if !elem.attributes['deleted']
2782                 append_dir_elem.call(child_iter, elem)
2783             end
2784         }
2785     }
2786     append_dir_elem.call(nil, xmldir)
2787     show_password_protections
2788
2789     $albums_tv.expand_all
2790     if select_first
2791         $albums_tv.selection.select_iter($albums_ts.iter_first)
2792     end
2793 end
2794
2795 def select_current_theme
2796     select_theme($xmldoc.root.attributes['theme'],
2797                  $xmldoc.root.attributes['limit-sizes'],
2798                  !$xmldoc.root.attributes['optimize-for-32'].nil?,
2799                  $xmldoc.root.attributes['thumbnails-per-row'])
2800 end
2801
2802 def open_file(filename)
2803
2804     $filename = nil
2805     $modified = false
2806     $current_path = nil   #- invalidate
2807     $modified_pixbufs = {}
2808     $albums_ts.clear
2809     $autotable.clear
2810     $subalbums_vb.children.each { |chld|
2811         $subalbums_vb.remove(chld)
2812     }
2813
2814     if !File.exists?(filename)
2815         return utf8(_("File not found."))
2816     end
2817
2818     begin
2819         $xmldoc = REXML::Document.new File.new(filename)
2820     rescue Exception
2821         $xmldoc = nil
2822     end
2823
2824     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2825         if entry2type(filename).nil?
2826             return utf8(_("Not a booh file!"))
2827         else
2828             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."))
2829         end
2830     end
2831
2832     if !source = $xmldoc.root.attributes['source']
2833         return utf8(_("Corrupted booh file..."))
2834     end
2835
2836     if !dest = $xmldoc.root.attributes['destination']
2837         return utf8(_("Corrupted booh file..."))
2838     end
2839
2840     if !theme = $xmldoc.root.attributes['theme']
2841         return utf8(_("Corrupted booh file..."))
2842     end
2843
2844     if $xmldoc.root.attributes['version'] < '0.8.99.2'
2845         msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2846         mark_document_as_dirty
2847         if $xmldoc.root.attributes['version'] < '0.8.4'
2848             msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2849             `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2850                 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2851                 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2852                 if old_dest_dir != new_dest_dir
2853                     sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2854                 end
2855                 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2856                     xmldir.elements.each { |element|
2857                         if %w(image video).include?(element.name) && !element.attributes['deleted']
2858                             old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2859                             new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2860                             Dir[old_name + '*'].each { |file|
2861                                 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2862                                 file != new_file and sys("mv '#{file}' '#{new_file}'")
2863                             }
2864                         end
2865                         if element.name == 'dir' && !element.attributes['deleted']
2866                             old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2867                             new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2868                             old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2869                         end
2870                     }
2871                 else
2872                     msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2873                 end
2874             }
2875         end
2876         $xmldoc.root.add_attribute('version', $VERSION)
2877     end
2878
2879     select_current_theme
2880
2881     $filename = filename
2882     set_mainwindow_title(nil)
2883     $default_size['thumbnails'] =~ /(.*)x(.*)/
2884     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2885     $albums_thumbnail_size =~ /(.*)x(.*)/
2886     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2887
2888     populate_subalbums_treeview(true)
2889
2890     $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
2891     return nil
2892 end
2893
2894 def open_file_user(filename)
2895     result = open_file(filename)
2896     if !result
2897         $config['last-opens'] ||= []
2898         if $config['last-opens'][-1] != utf8(filename)
2899             $config['last-opens'] << utf8(filename)
2900         end
2901         $orig_filename = $filename
2902         tmp = Tempfile.new("boohtemp")
2903         tmp.close!
2904         #- for security
2905         ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2906         ios.close
2907         $tempfiles << $filename << "#{$filename}.backup"
2908     else
2909         $orig_filename = nil
2910     end
2911     return result
2912 end
2913
2914 def open_file_popup
2915     if !ask_save_modifications(utf8(_("Save this album?")),
2916                                utf8(_("Do you want to save the changes to this album?")),
2917                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2918         return
2919     end
2920     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2921                                     nil,
2922                                     Gtk::FileChooser::ACTION_OPEN,
2923                                     nil,
2924                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2925     fc.add_shortcut_folder(File.expand_path("~/.booh"))
2926     fc.set_current_folder(File.expand_path("~/.booh"))
2927     fc.transient_for = $main_window
2928     ok = false
2929     while !ok
2930         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2931             push_mousecursor_wait(fc)
2932             msg = open_file_user(fc.filename)
2933             pop_mousecursor(fc)
2934             if msg
2935                 show_popup(fc, msg)
2936                 ok = false
2937             else
2938                 ok = true
2939             end
2940         else
2941             ok = true
2942         end
2943     end
2944     fc.destroy
2945 end
2946
2947 def additional_booh_options
2948     options = ''
2949     if $config['mproc']
2950         options += "--mproc #{$config['mproc'].to_i} "
2951     end
2952     options += "--comments-format '#{$config['comments-format']}' "
2953     if $config['transcode-videos']
2954         options += "--transcode-videos '#{$config['transcode-videos']}' "
2955     end
2956     return options
2957 end
2958
2959 def ask_multi_languages(value)
2960     if ! value.nil?
2961         spl = value.split(',')
2962         value = [ spl[0..-2], spl[-1] ]
2963     end
2964
2965     dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
2966                              $main_window,
2967                              Gtk::Dialog::MODAL,
2968                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2969                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2970
2971     lbl = Gtk::Label.new
2972     lbl.markup = utf8(
2973 _("You can choose to activate <b>multi-languages</b> support for this web-album
2974 (it will work only if you publish your web-album on an Apache web-server). This will
2975 use the MultiViews feature of Apache; the pages will be served according to the
2976 value of the Accept-Language HTTP header sent by the web browsers, so that people
2977 with different languages preferences will be able to browse your web-album with
2978 navigation in their language (if language is available).
2979 "))
2980
2981     dialog.vbox.add(lbl)
2982     dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
2983                                                                          add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
2984                                                                                                      add(languages = Gtk::Button.new))))
2985
2986     pick_languages = proc {
2987         dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
2988                                   $main_window,
2989                                   Gtk::Dialog::MODAL,
2990                                   [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2991                                   [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2992
2993         dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
2994         hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
2995         cbs = []
2996         SUPPORTED_LANGUAGES.each { |lang|
2997             hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
2998             if ! value.nil? && value[0].include?(lang)
2999                 cb.active = true
3000             end
3001             cbs << [ lang, cb ]
3002         }
3003
3004         dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3005         hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3006         fallback_language = nil
3007         hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3008         fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3009         if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3010             fbl_rb.active = true
3011             fallback_language = SUPPORTED_LANGUAGES[0]
3012         end
3013         SUPPORTED_LANGUAGES[1..-1].each { |lang|
3014             hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3015             rb.signal_connect('clicked') { fallback_language = lang }
3016             if ! value.nil? && value[1] == lang
3017                 rb.active = true
3018             end
3019         }
3020
3021         dialog2.window_position = Gtk::Window::POS_MOUSE
3022         dialog2.show_all
3023
3024         resp = nil
3025         dialog2.run { |response|
3026             resp = response
3027             if resp == Gtk::Dialog::RESPONSE_OK
3028                 value = []
3029                 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3030                 value[1] = fallback_language
3031                 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| utf8(langname(v)) }.join(', '), utf8(langname(value[1])) ])
3032             end
3033             dialog2.destroy
3034         }
3035         resp
3036     }
3037
3038     languages.signal_connect('clicked') {
3039         pick_languages.call
3040     }
3041     dialog.window_position = Gtk::Window::POS_MOUSE
3042     if value.nil?
3043         rb_no.active = true
3044     else
3045         rb_yes.active = true
3046         languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| utf8(langname(v)) }.join(', '), utf8(langname(value[1])) ])
3047     end
3048     rb_no.signal_connect('clicked') {
3049         if rb_no.active?
3050             languages.hide
3051         else
3052             if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3053                 rb_no.activate
3054             else
3055                 languages.show
3056             end
3057         end
3058     }
3059     oldval = value
3060     dialog.show_all
3061     if rb_no.active?
3062         languages.hide
3063     end
3064
3065     dialog.run { |response|
3066         if rb_no.active?
3067             value = nil
3068         end
3069         dialog.destroy
3070         if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3071             if value.nil?
3072                 return [ true, nil ]
3073             else
3074                 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3075             end
3076         else
3077             return [ false ]
3078         end
3079     }
3080 end
3081
3082 def new_album
3083     if !ask_save_modifications(utf8(_("Save this album?")),
3084                                utf8(_("Do you want to save the changes to this album?")),
3085                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3086         return
3087     end
3088     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3089                              $main_window,
3090                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3091                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3092                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3093     
3094     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3095     tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3096                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3097     tbl.attach(src = Gtk::Entry.new,
3098                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3099     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3100                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3101     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3102                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3103     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3104                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3105     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3106                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3107     tbl.attach(dest = Gtk::Entry.new,
3108                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3109     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3110                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3111     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3112                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3113     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3114                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3115     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3116                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3117
3118     tooltips = Gtk::Tooltips.new
3119     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3120     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3121                          pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3122     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3123                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3124     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
3125     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 non digital cameras are 3/2 when pictures from digital cameras are 4/3)")), nil)
3126     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3127                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3128     nperpage_model = Gtk::ListStore.new(String, String)
3129     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3130                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3131     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3132     nperpagecombo.set_attributes(crt, { :markup => 0 })
3133     iter = nperpage_model.append
3134     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3135     iter[1] = nil
3136     [ 12, 20, 30, 40, 50 ].each { |v|
3137         iter = nperpage_model.append
3138         iter[0] = iter[1] = v.to_s
3139     }
3140     nperpagecombo.active = 0
3141
3142     multilanguages_value = nil
3143     vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new(utf8(_("Multi-languages: disabled."))), false, false, 0).
3144                                         pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3145     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)
3146     multilanguages.signal_connect('clicked') {
3147         retval = ask_multi_languages(multilanguages_value)
3148         if retval[0] 
3149             multilanguages_value = retval[1]
3150         end
3151         if multilanguages_value
3152             ml_label.text = utf8(_("Multi-languages: enabled."))
3153         else
3154             ml_label.text = utf8(_("Multi-languages: disabled."))
3155         end
3156     }
3157
3158     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3159                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3160     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)
3161     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3162                                    pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
3163     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)
3164
3165     src_nb_calculated_for = ''
3166     src_nb_thread = nil
3167     process_src_nb = proc {
3168         if src.text != src_nb_calculated_for
3169             src_nb_calculated_for = src.text
3170             if src_nb_thread
3171                 Thread.kill(src_nb_thread)
3172                 src_nb_thread = nil
3173             end
3174             if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3175                 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3176             else
3177                 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3178                     if File.readable?(from_utf8_safe(src_nb_calculated_for))
3179                         src_nb_thread = Thread.new {
3180                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3181                             total = { 'image' => 0, 'video' => 0, nil => 0 }
3182                             `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3183                                 if File.basename(dir) =~ /^\./
3184                                     next
3185                                 else
3186                                     begin
3187                                         Dir.entries(dir.chomp).each { |file|
3188                                             total[entry2type(file)] += 1
3189                                         }
3190                                     rescue Errno::EACCES, Errno::ENOENT
3191                                     end
3192                                 end
3193                             }
3194                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3195                             src_nb_thread = nil
3196                         }
3197                     else
3198                         src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3199                     end
3200                 else
3201                     src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3202                 end
3203             end
3204         end
3205         true
3206     }
3207     timeout_src_nb = Gtk.timeout_add(100) {
3208         process_src_nb.call
3209     }
3210
3211     src_browse.signal_connect('clicked') {
3212         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3213                                         nil,
3214                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
3215                                         nil,
3216                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3217         fc.transient_for = $main_window
3218         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3219             src.text = utf8(fc.filename)
3220             process_src_nb.call
3221             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3222         end
3223         fc.destroy
3224     }
3225
3226     dest_browse.signal_connect('clicked') {
3227         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3228                                         nil,
3229                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
3230                                         nil,
3231                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3232         fc.transient_for = $main_window
3233         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3234             dest.text = utf8(fc.filename)
3235         end
3236         fc.destroy
3237     }
3238
3239     conf_browse.signal_connect('clicked') {
3240         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3241                                         nil,
3242                                         Gtk::FileChooser::ACTION_SAVE,
3243                                         nil,
3244                                         [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3245         fc.transient_for = $main_window
3246         fc.add_shortcut_folder(File.expand_path("~/.booh"))
3247         fc.set_current_folder(File.expand_path("~/.booh"))
3248         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3249             conf.text = utf8(fc.filename)
3250         end
3251         fc.destroy
3252     }
3253
3254     theme_sizes = []
3255     nperrows = []
3256     recreate_theme_config = proc {
3257         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3258         theme_sizes = []
3259         select_theme(theme_button.label, 'all', optimize432.active?, nil)
3260         $images_size.each { |s|
3261             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3262             if !s['optional']
3263                 cb.active = true
3264             end
3265             tooltips.set_tip(cb, utf8(s['description']), nil)
3266             theme_sizes << { :widget => cb, :value => s['name'] }
3267         }
3268         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3269         tooltips = Gtk::Tooltips.new
3270         tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3271         theme_sizes << { :widget => cb, :value => 'original' }
3272         sizes.show_all
3273
3274         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3275         nperrow_group = nil
3276         nperrows = []
3277         $allowed_N_values.each { |n|
3278             if nperrow_group
3279                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3280             else
3281                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3282             end
3283             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3284             if $default_N == n
3285                 rb.active = true
3286             end
3287             nperrows << { :widget => rb, :value => n }
3288         }
3289         nperrowradios.show_all
3290     }
3291     recreate_theme_config.call
3292
3293     theme_button.signal_connect('clicked') {
3294         if newtheme = theme_choose(theme_button.label)
3295             theme_button.label = newtheme
3296             recreate_theme_config.call
3297         end
3298     }
3299
3300     dialog.vbox.add(frame1)
3301     dialog.vbox.add(frame2)
3302     dialog.show_all
3303
3304     keepon = true
3305     ok = true
3306     while keepon
3307         dialog.run { |response|
3308             if response == Gtk::Dialog::RESPONSE_OK
3309                 srcdir = from_utf8_safe(src.text)
3310                 destdir = from_utf8_safe(dest.text)
3311                 confpath = from_utf8_safe(conf.text)
3312                 if src.text != '' && srcdir == ''
3313                     show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3314                     src.grab_focus
3315                 elsif !File.directory?(srcdir)
3316                     show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3317                     src.grab_focus
3318                 elsif dest.text != '' && destdir == ''
3319                     show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3320                     dest.grab_focus
3321                 elsif destdir != make_dest_filename(destdir)
3322                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3323                     dest.grab_focus
3324                 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3325                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3326 inside it will be permanently removed before creating the web-album!
3327 Are you sure you want to continue?")), { :okcancel => true })
3328                     dest.grab_focus
3329                 elsif File.exists?(destdir) && !File.directory?(destdir)
3330                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3331                     dest.grab_focus
3332                 elsif conf.text == ''
3333                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3334                     conf.grab_focus
3335                 elsif conf.text != '' && confpath == ''
3336                     show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3337                     conf.grab_focus
3338                 elsif File.directory?(confpath)
3339                     show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3340                     conf.grab_focus
3341                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3342                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3343                 else
3344                     system("mkdir '#{destdir}'")
3345                     if !File.directory?(destdir)
3346                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3347                         dest.grab_focus
3348                     else
3349                         keepon = false
3350                     end
3351                 end
3352             else
3353                 keepon = ok = false
3354             end
3355         }
3356     end
3357     if ok
3358         srcdir = from_utf8(src.text)
3359         destdir = from_utf8(dest.text)
3360         configskel = File.expand_path(from_utf8(conf.text))
3361         theme = theme_button.label
3362         #- some sort of automatic theme preference
3363         $config['default-theme'] = theme
3364         sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3365         nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3366         nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3367         opt432 = optimize432.active?
3368         madewith = madewithentry.text.gsub('\'', '&#39;')  #- because the parameters to booh-backend are between apostrophes
3369         indexlink = indexlinkentry.text.gsub('\'', '&#39;')
3370     end
3371     if src_nb_thread
3372         Thread.kill(src_nb_thread)
3373         gtk_thread_flush  #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3374     end
3375     dialog.destroy
3376     Gtk.timeout_remove(timeout_src_nb)
3377
3378     if ok
3379         call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3380                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3381                      (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3382                      (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
3383                      "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3384                      utf8(_("Please wait while scanning source directory...")),
3385                      'full scan',
3386                      { :closure_after => proc { open_file_user(configskel) } })
3387     end
3388 end
3389
3390 def properties
3391     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3392                              $main_window,
3393                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3394                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3395                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3396     
3397     source = $xmldoc.root.attributes['source']
3398     dest = $xmldoc.root.attributes['destination']
3399     theme = $xmldoc.root.attributes['theme']
3400     opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3401     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3402     nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3403     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3404     if limit_sizes
3405         limit_sizes = limit_sizes.split(/,/)
3406     end
3407     madewith = ($xmldoc.root.attributes['made-with'] || '').gsub('&#39;', '\'')
3408     indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub('&#39;', '\'')
3409     save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3410
3411     tooltips = Gtk::Tooltips.new
3412     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3413     tbl.attach(Gtk::Label.new(utf8(_("Directory of source photos/videos: "))),
3414                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3415     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3416                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3417     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3418                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3419     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3420                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3421     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3422                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3423     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3424                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3425
3426     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3427     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3428                                    pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3429     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3430                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3431     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3432     tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of pictures from non digital cameras are 3/2 when pictures from digital cameras are 4/3)")), nil)
3433     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3434                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3435     nperpage_model = Gtk::ListStore.new(String, String)
3436     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3437                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3438     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3439     nperpagecombo.set_attributes(crt, { :markup => 0 })
3440     iter = nperpage_model.append
3441     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3442     iter[1] = nil
3443     [ 12, 20, 30, 40, 50 ].each { |v|
3444         iter = nperpage_model.append
3445         iter[0] = iter[1] = v.to_s
3446         if nperpage && nperpage == v.to_s
3447             nperpagecombo.active_iter = iter
3448         end
3449     }
3450     if nperpagecombo.active_iter.nil?
3451         nperpagecombo.active = 0
3452     end
3453
3454     vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3455                                         pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3456     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)
3457     ml_update = proc {
3458         if save_multilanguages_value
3459             ml_label.text = utf8(_("Multi-languages: enabled."))
3460         else
3461             ml_label.text = utf8(_("Multi-languages: disabled."))
3462         end
3463     }
3464     ml_update.call
3465     multilanguages.signal_connect('clicked') {
3466         retval = ask_multi_languages(save_multilanguages_value)
3467         if retval[0] 
3468             save_multilanguages_value = retval[1]
3469         end
3470         ml_update.call
3471     }
3472
3473     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3474                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3475     if indexlink
3476         indexlinkentry.text = indexlink
3477     end
3478     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)
3479     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3480                                    pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3481     if madewith
3482         madewithentry.text = madewith
3483     end
3484     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)
3485
3486     theme_sizes = []
3487     nperrows = []
3488     recreate_theme_config = proc {
3489         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3490         theme_sizes = []
3491         select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3492
3493         $images_size.each { |s|
3494             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3495             if limit_sizes
3496                 if limit_sizes.include?(s['name'])
3497                     cb.active = true
3498                 end
3499             else
3500                 if !s['optional']
3501                     cb.active = true
3502                 end
3503             end
3504             tooltips.set_tip(cb, utf8(s['description']), nil)
3505             theme_sizes << { :widget => cb, :value => s['name'] }
3506         }
3507         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3508         tooltips = Gtk::Tooltips.new
3509         tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3510         if limit_sizes && limit_sizes.include?('original')
3511             cb.active = true