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