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