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