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