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