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