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