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