fbb4cafc506e03d644db0115faac1aa1874c653a
[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 remove_all_captions
1885     $modified = true
1886     texts = {}
1887     $autotable.current_order.each { |path|
1888         texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
1889         $name2widgets[File.basename(path)][:textview].buffer.text = ''
1890     }
1891     save_undo(_("remove all captions"),
1892               Proc.new { |texts|
1893                   texts.each_key { |key|
1894                       $name2widgets[key][:textview].buffer.text = texts[key]
1895                   }
1896                   $notebook.set_page(1)
1897                   Proc.new {
1898                       texts.each_key { |key|
1899                           $name2widgets[key][:textview].buffer.text = ''
1900                       }
1901                       $notebook.set_page(1)
1902                   }
1903               }, texts)
1904 end
1905
1906 def change_dir
1907     $selected_elements.each_key { |path|
1908         $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1909     }
1910     $autotable.clear
1911     $vbox2widgets = {}
1912     $name2widgets = {}
1913     $name2closures = {}
1914     $selected_elements = {}
1915     $cuts = []
1916     $multiple_dnd = []
1917     UndoHandler.cleanup
1918     $undo_tb.sensitive = $undo_mb.sensitive = false
1919     $redo_tb.sensitive = $redo_mb.sensitive = false
1920
1921     if !$current_path
1922         return
1923     end
1924
1925     $subalbums_vb.children.each { |chld|
1926         $subalbums_vb.remove(chld)
1927     }
1928     $subalbums = Gtk::Table.new(0, 0, true)
1929     current_y_sub_albums = 0
1930
1931     $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
1932     $subalbums_edits = {}
1933     subalbums_counter = 0
1934     subalbums_edits_bypos = {}
1935
1936     add_subalbum = Proc.new { |xmldir, counter|
1937         $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
1938         subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
1939         if xmldir == $xmldir
1940             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
1941             caption = xmldir.attributes['thumbnails-caption']
1942             captionfile, dummy = find_subalbum_caption_info(xmldir)
1943             infotype = 'thumbnails'
1944         else
1945             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
1946             captionfile, caption = find_subalbum_caption_info(xmldir)
1947             infotype = find_subalbum_info_type(xmldir)
1948         end
1949         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
1950         hbox = Gtk::HBox.new
1951         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
1952         f = Gtk::Frame.new
1953         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
1954
1955         img = nil
1956         my_gen_real_thumbnail = proc {
1957             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
1958         }
1959
1960         if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
1961             f.add(img = Gtk::Image.new)
1962             my_gen_real_thumbnail.call
1963         else
1964             f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
1965         end
1966         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
1967         $subalbums.attach(hbox,
1968                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1969
1970         frame, textview = create_editzone($subalbums_sw, 0, img)
1971         textview.buffer.text = caption
1972         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
1973                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1974
1975         change_image = Proc.new {
1976             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
1977                                             nil,
1978                                             Gtk::FileChooser::ACTION_OPEN,
1979                                             nil,
1980                                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1981             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
1982             fc.transient_for = $main_window
1983             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))
1984             f.add(preview_img = Gtk::Image.new)
1985             preview.show_all
1986             fc.signal_connect('update-preview') { |w|
1987                 begin
1988                     if fc.preview_filename
1989                         preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
1990                         fc.preview_widget_active = true
1991                     end
1992                 rescue Gdk::PixbufError
1993                     fc.preview_widget_active = false
1994                 end
1995             }
1996             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1997                 $modified = true
1998                 old_file = captionfile
1999                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2000                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2001                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2002                 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2003
2004                 new_file = fc.filename
2005                 msg 3, "new captionfile is: #{fc.filename}"
2006                 perform_changefile = Proc.new {
2007                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2008                     $modified_pixbufs.delete(thumbnail_file)
2009                     xmldir.delete_attribute("#{infotype}-rotate")
2010                     xmldir.delete_attribute("#{infotype}-color-swap")
2011                     xmldir.delete_attribute("#{infotype}-enhance")
2012                     xmldir.delete_attribute("#{infotype}-frame-offset")
2013                     my_gen_real_thumbnail.call
2014                 }
2015                 perform_changefile.call
2016
2017                 save_undo(_("change caption file for sub-album"),
2018                           Proc.new {
2019                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2020                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2021                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2022                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2023                               xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2024                               my_gen_real_thumbnail.call
2025                               $notebook.set_page(0)
2026                               Proc.new {
2027                                   perform_changefile.call
2028                                   $notebook.set_page(0)
2029                               }
2030                           })
2031             end
2032             fc.destroy
2033         }
2034
2035         rotate_and_cleanup = Proc.new { |angle|
2036             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2037             system("rm -f '#{thumbnail_file}'")
2038         }
2039
2040         move = Proc.new { |direction|
2041             $modified = true
2042
2043             save_changes('forced')
2044             if direction == 'up'
2045                 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2046                 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2047                 subalbums_edits_bypos[oldpos - 1][:position] += 1
2048             else
2049                 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2050                 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2051                 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2052             end
2053
2054             elems = []
2055             $xmldir.elements.each('dir') { |element|
2056                 if (!element.attributes['deleted'])
2057                     elems << [ element.attributes['path'], element.remove ]
2058                 end
2059             }
2060             elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2061                   each { |e| $xmldir.add_element(e[1]) }
2062             #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2063             $xmldir.elements.each('descendant::dir') { |elem|
2064                 elem.delete_attribute('already-generated')
2065             }
2066             change_dir
2067         }
2068
2069         color_swap_and_cleanup = Proc.new {
2070             perform_color_swap_and_cleanup = Proc.new {
2071                 color_swap(xmldir, "#{infotype}-")
2072                 my_gen_real_thumbnail.call
2073             }
2074             perform_color_swap_and_cleanup.call
2075
2076             save_undo(_("color swap"),
2077                       Proc.new {
2078                           perform_color_swap_and_cleanup.call
2079                           $notebook.set_page(0)
2080                           Proc.new {
2081                               perform_color_swap_and_cleanup.call
2082                               $notebook.set_page(0)
2083                           }
2084                       })
2085         }
2086
2087         change_frame_offset_and_cleanup = Proc.new {
2088             if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2089                 perform_change_frame_offset_and_cleanup = Proc.new { |val|
2090                     change_frame_offset(xmldir, "#{infotype}-", val)
2091                     my_gen_real_thumbnail.call
2092                 }
2093                 perform_change_frame_offset_and_cleanup.call(values[:new])
2094
2095                 save_undo(_("specify frame offset"),
2096                           Proc.new {
2097                               perform_change_frame_offset_and_cleanup.call(values[:old])
2098                               $notebook.set_page(0)
2099                               Proc.new {
2100                                   perform_change_frame_offset_and_cleanup.call(values[:new])
2101                                   $notebook.set_page(0)
2102                               }
2103                           })
2104             end
2105         }
2106
2107         whitebalance_and_cleanup = Proc.new {
2108             if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2109                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2110                 perform_change_whitebalance_and_cleanup = Proc.new { |val|
2111                     change_whitebalance(xmldir, "#{infotype}-", val)
2112                     recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2113                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2114                     system("rm -f '#{thumbnail_file}'")
2115                 }
2116                 perform_change_whitebalance_and_cleanup.call(values[:new])
2117                 
2118                 save_undo(_("fix white balance"),
2119                           Proc.new {
2120                               perform_change_whitebalance_and_cleanup.call(values[:old])
2121                               $notebook.set_page(0)
2122                               Proc.new {
2123                                   perform_change_whitebalance_and_cleanup.call(values[:new])
2124                                   $notebook.set_page(0)
2125                               }
2126                           })
2127             end
2128         }
2129
2130         enhance_and_cleanup = Proc.new {
2131             perform_enhance_and_cleanup = Proc.new {
2132                 enhance(xmldir, "#{infotype}-")
2133                 my_gen_real_thumbnail.call
2134             }
2135             
2136             perform_enhance_and_cleanup.call
2137             
2138             save_undo(_("enhance"),
2139                       Proc.new {
2140                           perform_enhance_and_cleanup.call
2141                           $notebook.set_page(0)
2142                           Proc.new {
2143                               perform_enhance_and_cleanup.call
2144                               $notebook.set_page(0)
2145                           }
2146                       })
2147         }
2148
2149         evtbox.signal_connect('button-press-event') { |w, event|
2150             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2151                 if $r90.active?
2152                     rotate_and_cleanup.call(90)
2153                 elsif $r270.active?
2154                     rotate_and_cleanup.call(-90)
2155                 elsif $enhance.active?
2156                     enhance_and_cleanup.call
2157                 end
2158             end
2159             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2160                 popup_thumbnail_menu(event, ['change_image'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2161                                      { :forbid_left => true, :forbid_right => true,
2162                                        :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter },
2163                                      { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2164                                        :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
2165             end
2166             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2167                 change_image.call
2168                 true   #- handled
2169             end
2170         }
2171         evtbox.signal_connect('button-press-event') { |w, event|
2172             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2173             false
2174         }
2175
2176         evtbox.signal_connect('button-release-event') { |w, event|
2177             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2178                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2179                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2180                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2181                     msg 3, "gesture rotate: #{angle}"
2182                     rotate_and_cleanup.call(angle)
2183                 end
2184             end
2185             $gesture_press = nil
2186         }
2187                 
2188         $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2189         $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2190         current_y_sub_albums += 1
2191     }
2192
2193     if $xmldir.child_byname_notattr('dir', 'deleted')
2194         #- title edition
2195         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2196         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2197         $subalbums_title.set_justification(Gtk::Justification::CENTER)
2198         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2199         #- this album image/caption
2200         if $xmldir.attributes['thumbnails-caption']
2201             add_subalbum.call($xmldir, 0)
2202         end
2203     end
2204     total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2205     $xmldir.elements.each { |element|
2206         if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2207             #- element (image or video) of this album
2208             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2209             msg 3, "dest_img: #{dest_img}"
2210             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
2211             total[element.name] += 1
2212         end
2213         if element.name == 'dir' && !element.attributes['deleted']
2214             #- sub-album image/caption
2215             add_subalbum.call(element, subalbums_counter += 1)
2216             total[element.name] += 1
2217         end
2218     }
2219     $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2220                                                                                 total['image'], total['video'], total['dir'] ]))
2221     $subalbums_vb.add($subalbums)
2222     $subalbums_vb.show_all
2223
2224     if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2225         $notebook.get_tab_label($autotable_sw).sensitive = false
2226         $notebook.set_page(0)
2227         $thumbnails_title.buffer.text = ''
2228     else
2229         $notebook.get_tab_label($autotable_sw).sensitive = true
2230         $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2231     end
2232
2233     if !$xmldir.child_byname_notattr('dir', 'deleted')
2234         $notebook.get_tab_label($subalbums_sw).sensitive = false
2235         $notebook.set_page(1)
2236     else
2237         $notebook.get_tab_label($subalbums_sw).sensitive = true
2238     end
2239 end
2240
2241 def pixbuf_or_nil(filename)
2242     begin
2243         return Gdk::Pixbuf.new(filename)
2244     rescue
2245         return nil
2246     end
2247 end
2248
2249 def theme_choose(current)
2250     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2251                              $main_window,
2252                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2253                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2254                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2255
2256     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2257     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2258     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2259     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2260     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2261     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2262     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2263     treeview.signal_connect('button-press-event') { |w, event|
2264         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2265             dialog.response(Gtk::Dialog::RESPONSE_OK)
2266         end
2267     }
2268
2269     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2270
2271     `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2272         dir.chomp!
2273         iter = model.append
2274         iter[0] = File.basename(dir)
2275         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2276         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2277         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2278         if File.basename(dir) == current
2279             treeview.selection.select_iter(iter)
2280         end
2281     }
2282
2283     dialog.set_default_size(700, 400)
2284     dialog.vbox.show_all
2285     dialog.run { |response|
2286         iter = treeview.selection.selected
2287         dialog.destroy
2288         if response == Gtk::Dialog::RESPONSE_OK && iter
2289             return model.get_value(iter, 0)
2290         end
2291     }
2292     return nil
2293 end
2294
2295 def show_password_protections
2296     examine_dir_elem = Proc.new { |parent_iter, xmldir, already_protected|
2297         child_iter = $albums_iters[xmldir.attributes['path']]
2298         if xmldir.attributes['password-protect']
2299             child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2300             already_protected = true
2301         elsif already_protected
2302             pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2303             if pix
2304                 pix = pix.saturate_and_pixelate(1, true)
2305             end
2306             child_iter[2] = pix
2307         else
2308             child_iter[2] = nil
2309         end
2310         xmldir.elements.each('dir') { |elem|
2311             if !elem.attributes['deleted']
2312                 examine_dir_elem.call(child_iter, elem, already_protected)
2313             end
2314         }
2315     }
2316     examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2317 end
2318
2319 def populate_subalbums_treeview(select_first)
2320     $albums_ts.clear
2321     $autotable.clear
2322     $albums_iters = {}
2323     $subalbums_vb.children.each { |chld|
2324         $subalbums_vb.remove(chld)
2325     }
2326
2327     source = $xmldoc.root.attributes['source']
2328     msg 3, "source: #{source}"
2329
2330     xmldir = $xmldoc.elements['//dir']
2331     if !xmldir || xmldir.attributes['path'] != source
2332         msg 1, _("Corrupted booh file...")
2333         return
2334     end
2335
2336     append_dir_elem = Proc.new { |parent_iter, xmldir|
2337         child_iter = $albums_ts.append(parent_iter)
2338         child_iter[0] = File.basename(xmldir.attributes['path'])
2339         child_iter[1] = xmldir.attributes['path']
2340         $albums_iters[xmldir.attributes['path']] = child_iter
2341         msg 3, "puttin location: #{xmldir.attributes['path']}"
2342         xmldir.elements.each('dir') { |elem|
2343             if !elem.attributes['deleted']
2344                 append_dir_elem.call(child_iter, elem)
2345             end
2346         }
2347     }
2348     append_dir_elem.call(nil, xmldir)
2349     show_password_protections
2350
2351     $albums_tv.expand_all
2352     if select_first
2353         $albums_tv.selection.select_iter($albums_ts.iter_first)
2354     end
2355 end
2356
2357 def open_file(filename)
2358
2359     $filename = nil
2360     $modified = false
2361     $current_path = nil   #- invalidate
2362     $modified_pixbufs = {}
2363     $albums_ts.clear
2364     $autotable.clear
2365     $subalbums_vb.children.each { |chld|
2366         $subalbums_vb.remove(chld)
2367     }
2368
2369     if !File.exists?(filename)
2370         return utf8(_("File not found."))
2371     end
2372
2373     begin
2374         $xmldoc = REXML::Document.new File.new(filename)
2375     rescue Exception
2376         $xmldoc = nil
2377     end
2378
2379     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2380         if entry2type(filename).nil?
2381             return utf8(_("Not a booh file!"))
2382         else
2383             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."))
2384         end
2385     end
2386
2387     if !source = $xmldoc.root.attributes['source']
2388         return utf8(_("Corrupted booh file..."))
2389     end
2390
2391     if !dest = $xmldoc.root.attributes['destination']
2392         return utf8(_("Corrupted booh file..."))
2393     end
2394
2395     if !theme = $xmldoc.root.attributes['theme']
2396         return utf8(_("Corrupted booh file..."))
2397     end
2398
2399     if $xmldoc.root.attributes['version'] < '0.8.4'
2400         msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2401         mark_document_as_dirty
2402         if $xmldoc.root.attributes['version'] < '0.8.4'
2403             msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2404             `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2405                 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2406                 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2407                 if old_dest_dir != new_dest_dir
2408                     sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2409                 end
2410                 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2411                     xmldir.elements.each { |element|
2412                         if %w(image video).include?(element.name) && !element.attributes['deleted']
2413                             old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2414                             new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2415                             Dir[old_name + '*'].each { |file|
2416                                 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2417                                 file != new_file and sys("mv '#{file}' '#{new_file}'")
2418                             }
2419                         end
2420                         if element.name == 'dir' && !element.attributes['deleted']
2421                             old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2422                             new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2423                             old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2424                         end
2425                     }
2426                 else
2427                     msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2428                 end
2429             }
2430         end
2431         $xmldoc.root.add_attribute('version', $VERSION)
2432     end
2433
2434     limit_sizes = $xmldoc.root.attributes['limit-sizes']
2435     optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2436     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2437
2438     $filename = filename
2439     select_theme(theme, limit_sizes, optimizefor32, nperrow)
2440     $default_size['thumbnails'] =~ /(.*)x(.*)/
2441     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2442     $albums_thumbnail_size =~ /(.*)x(.*)/
2443     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2444
2445     populate_subalbums_treeview(true)
2446
2447     $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = true
2448     return nil
2449 end
2450
2451 def open_file_user(filename)
2452     result = open_file(filename)
2453     if !result
2454         $config['last-opens'] ||= []
2455         if $config['last-opens'][-1] != utf8(filename)
2456             $config['last-opens'] << utf8(filename)
2457         end
2458         $orig_filename = $filename
2459         tmp = Tempfile.new("boohtemp")
2460         tmp.close!
2461         #- for security
2462         ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2463         ios.close
2464         $tempfiles << $filename << "#{$filename}.backup"
2465     else
2466         $orig_filename = nil
2467     end
2468     return result
2469 end
2470
2471 def open_file_popup
2472     if !ask_save_modifications(utf8(_("Save this album?")),
2473                                utf8(_("Do you want to save the changes to this album?")),
2474                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2475         return
2476     end
2477     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2478                                     nil,
2479                                     Gtk::FileChooser::ACTION_OPEN,
2480                                     nil,
2481                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2482     fc.add_shortcut_folder(File.expand_path("~/.booh"))
2483     fc.set_current_folder(File.expand_path("~/.booh"))
2484     fc.transient_for = $main_window
2485     ok = false
2486     while !ok
2487         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2488             push_mousecursor_wait(fc)
2489             msg = open_file_user(fc.filename)
2490             pop_mousecursor(fc)
2491             if msg
2492                 show_popup(fc, msg)
2493                 ok = false
2494             else
2495                 ok = true
2496             end
2497         else
2498             ok = true
2499         end
2500     end
2501     fc.destroy
2502 end
2503
2504 def open_url(url)
2505     cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2506     msg 2, cmd
2507     system(cmd)
2508 end
2509
2510 def additional_booh_options
2511     options = ''
2512     if $config['mproc']
2513         options += "--mproc #{$config['mproc'].to_i} "
2514     end
2515     if $config['emptycomments']
2516         options += "--empty-comments "
2517     end
2518     return options
2519 end
2520
2521 def new_album
2522     if !ask_save_modifications(utf8(_("Save this album?")),
2523                                utf8(_("Do you want to save the changes to this album?")),
2524                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2525         return
2526     end
2527     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2528                              $main_window,
2529                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2530                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2531                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2532     
2533     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2534     tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2535                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2536     tbl.attach(src = Gtk::Entry.new,
2537                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2538     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2539                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2540     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2541                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2542     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2543                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2544     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2545                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2546     tbl.attach(dest = Gtk::Entry.new,
2547                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2548     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2549                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2550     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2551                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2552     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2553                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2554     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2555                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2556
2557     tooltips = Gtk::Tooltips.new
2558     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2559     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2560                          pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2561     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2562                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
2563     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2564     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)
2565     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2566                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2567     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2568                                    pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2569     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)
2570
2571     src_nb_calculated_for = ''
2572     src_nb_thread = nil
2573     process_src_nb = Proc.new {
2574         if src.text != src_nb_calculated_for
2575             src_nb_calculated_for = src.text
2576             if src_nb_thread
2577                 Thread.kill(src_nb_thread)
2578                 src_nb_thread = nil
2579             end
2580             if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2581                 if File.readable?(from_utf8(src_nb_calculated_for))
2582                     src_nb_thread = Thread.new {
2583                         gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2584                         total = { 'image' => 0, 'video' => 0, nil => 0 }
2585                         `find '#{from_utf8(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2586                             if File.basename(dir) =~ /^\./
2587                                 next
2588                             else
2589                                 begin
2590                                     Dir.entries(dir.chomp).each { |file|
2591                                         total[entry2type(file)] += 1
2592                                     }
2593                                 rescue Errno::EACCES, Errno::ENOENT
2594                                 end
2595                             end
2596                         }
2597                         gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2598                         src_nb_thread = nil
2599                     }
2600                 else
2601                     src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2602                 end
2603             else
2604                 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2605             end
2606         end
2607         true
2608     }
2609     timeout_src_nb = Gtk.timeout_add(100) {
2610         process_src_nb.call
2611     }
2612
2613     src_browse.signal_connect('clicked') {
2614         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2615                                         nil,
2616                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
2617                                         nil,
2618                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2619         fc.transient_for = $main_window
2620         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2621             src.text = utf8(fc.filename)
2622             process_src_nb.call
2623             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2624         end
2625         fc.destroy
2626     }
2627
2628     dest_browse.signal_connect('clicked') {
2629         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2630                                         nil,
2631                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
2632                                         nil,
2633                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2634         fc.transient_for = $main_window
2635         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2636             dest.text = utf8(fc.filename)
2637         end
2638         fc.destroy
2639     }
2640
2641     conf_browse.signal_connect('clicked') {
2642         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2643                                         nil,
2644                                         Gtk::FileChooser::ACTION_SAVE,
2645                                         nil,
2646                                         [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2647         fc.transient_for = $main_window
2648         fc.add_shortcut_folder(File.expand_path("~/.booh"))
2649         fc.set_current_folder(File.expand_path("~/.booh"))
2650         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2651             conf.text = utf8(fc.filename)
2652         end
2653         fc.destroy
2654     }
2655
2656     theme_sizes = []
2657     nperrows = []
2658     recreate_theme_config = proc {
2659         theme_sizes.each { |e| sizes.remove(e[:widget]) }
2660         theme_sizes = []
2661         select_theme(theme_button.label, 'all', optimize432.active?, nil)
2662         $images_size.each { |s|
2663             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2664             if !s['optional']
2665                 cb.active = true
2666             end
2667             tooltips.set_tip(cb, utf8(s['description']), nil)
2668             theme_sizes << { :widget => cb, :value => s['name'] }
2669         }
2670         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2671         tooltips = Gtk::Tooltips.new
2672         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2673         theme_sizes << { :widget => cb, :value => 'original' }
2674         sizes.show_all
2675
2676         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2677         nperrow_group = nil
2678         nperrows = []
2679         $allowed_N_values.each { |n|
2680             if nperrow_group
2681                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2682             else
2683                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2684             end
2685             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2686             if $default_N == n
2687                 rb.active = true
2688             end
2689             nperrows << { :widget => rb, :value => n }
2690         }
2691         nperrowradios.show_all
2692     }
2693     recreate_theme_config.call
2694
2695     theme_button.signal_connect('clicked') {
2696         if newtheme = theme_choose(theme_button.label)
2697             theme_button.label = newtheme
2698             recreate_theme_config.call
2699         end
2700     }
2701
2702     dialog.vbox.add(frame1)
2703     dialog.vbox.add(frame2)
2704     dialog.window_position = Gtk::Window::POS_MOUSE
2705     dialog.show_all
2706
2707     keepon = true
2708     ok = true
2709     while keepon
2710         dialog.run { |response|
2711             if response == Gtk::Dialog::RESPONSE_OK
2712                 srcdir = from_utf8(src.text)
2713                 destdir = from_utf8(dest.text)
2714                 if !File.directory?(srcdir)
2715                     show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2716                     src.grab_focus
2717                 elsif conf.text == ''
2718                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2719                     conf.grab_focus
2720                 elsif File.directory?(from_utf8(conf.text))
2721                     show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
2722                     conf.grab_focus
2723                 elsif destdir != make_dest_filename(destdir)
2724                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2725                     dest.grab_focus
2726                 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
2727                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2728                     dest.grab_focus
2729                 elsif File.exists?(destdir) && !File.directory?(destdir)
2730                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2731                     dest.grab_focus
2732                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2733                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2734                 else
2735                     system("mkdir '#{destdir}'")
2736                     if !File.directory?(destdir)
2737                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2738                         dest.grab_focus
2739                     else
2740                         keepon = false
2741                     end
2742                 end
2743             else
2744                 keepon = ok = false
2745             end
2746         }
2747     end
2748     srcdir = from_utf8(src.text)
2749     destdir = from_utf8(dest.text)
2750     configskel = File.expand_path(from_utf8(conf.text))
2751     theme = theme_button.label
2752     sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2753     nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2754     opt432 = optimize432.active?
2755     madewith = madewithentry.text
2756     if src_nb_thread
2757         Thread.kill(src_nb_thread)
2758         gtk_thread_flush  #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2759     end
2760     dialog.destroy
2761     Gtk.timeout_remove(timeout_src_nb)
2762
2763     if ok
2764         call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2765                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2766                      "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' #{additional_booh_options}",
2767                      utf8(_("Please wait while scanning source directory...")),
2768                      'full scan',
2769                      { :closure_after => proc { open_file_user(configskel) } })
2770     end
2771 end
2772
2773 def properties
2774     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2775                              $main_window,
2776                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2777                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2778                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2779     
2780     source = $xmldoc.root.attributes['source']
2781     dest = $xmldoc.root.attributes['destination']
2782     theme = $xmldoc.root.attributes['theme']
2783     opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2784     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2785     limit_sizes = $xmldoc.root.attributes['limit-sizes']
2786     if limit_sizes
2787         limit_sizes = limit_sizes.split(/,/)
2788     end
2789     madewith = $xmldoc.root.attributes['made-with']
2790
2791     tooltips = Gtk::Tooltips.new
2792     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2793     tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2794                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2795     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2796                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2797     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2798                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2799     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
2800                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2801     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2802                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2803     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
2804                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2805
2806     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2807     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2808                                    pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2809     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2810                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
2811     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2812     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)
2813     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2814                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2815     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2816                                    pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2817     if madewith
2818         madewithentry.text = madewith
2819     end
2820     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)
2821
2822     theme_sizes = []
2823     nperrows = []
2824     recreate_theme_config = proc {
2825         theme_sizes.each { |e| sizes.remove(e[:widget]) }
2826         theme_sizes = []
2827         select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
2828
2829         $images_size.each { |s|
2830             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2831             if limit_sizes
2832                 if limit_sizes.include?(s['name'])
2833                     cb.active = true
2834                 end
2835             else
2836                 if !s['optional']
2837                     cb.active = true
2838                 end
2839             end
2840             tooltips.set_tip(cb, utf8(s['description']), nil)
2841             theme_sizes << { :widget => cb, :value => s['name'] }
2842         }
2843         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2844         tooltips = Gtk::Tooltips.new
2845         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2846         if limit_sizes && limit_sizes.include?('original')
2847             cb.active = true
2848         end
2849         theme_sizes << { :widget => cb, :value => 'original' }
2850         sizes.show_all
2851
2852         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2853         nperrow_group = nil
2854         nperrows = []
2855         $allowed_N_values.each { |n|
2856             if nperrow_group
2857                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2858             else
2859                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2860             end
2861             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2862             nperrowradios.add(Gtk::Label.new('  '))
2863             if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
2864                 rb.active = true
2865             end
2866             nperrows << { :widget => rb, :value => n.to_s }
2867         }
2868         nperrowradios.show_all
2869     }
2870     recreate_theme_config.call
2871
2872     theme_button.signal_connect('clicked') {
2873         if newtheme = theme_choose(theme_button.label)
2874             limit_sizes = nil
2875             nperrow = nil
2876             theme_button.label = newtheme
2877             recreate_theme_config.call
2878         end
2879     }
2880
2881     dialog.vbox.add(frame1)
2882     dialog.vbox.add(frame2)
2883     dialog.window_position = Gtk::Window::POS_MOUSE
2884     dialog.show_all
2885
2886     keepon = true
2887     ok = true
2888     while keepon
2889         dialog.run { |response|
2890             if response == Gtk::Dialog::RESPONSE_OK
2891                 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2892                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2893                 else
2894                     keepon = false
2895                 end
2896             else
2897                 keepon = ok = false
2898             end
2899         }
2900     end
2901     save_theme = theme_button.label
2902     save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
2903     save_opt432 = optimize432.active?
2904     save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2905     save_madewith = madewithentry.text
2906     dialog.destroy
2907
2908     if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith)
2909         mark_document_as_dirty
2910         save_current_file
2911         call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
2912                      "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
2913                      "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' #{additional_booh_options}",
2914                      utf8(_("Please wait while scanning source directory...")),
2915                      'full scan',
2916                      { :closure_after => proc {
2917                              open_file($filename)
2918                              $modified = true
2919                          } })
2920     end
2921 end
2922
2923 def merge_current
2924     save_current_file
2925
2926     sel = $albums_tv.selection.selected_rows
2927
2928     call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2929                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
2930                  utf8(_("Please wait while scanning source directory...")),
2931                  'one dir scan',
2932                  { :closure_after => proc {
2933                          open_file($filename)
2934                          $albums_tv.selection.select_path(sel[0])
2935                          $modified = true
2936                      } })
2937 end
2938
2939 def merge_newsubs
2940     save_current_file
2941
2942     sel = $albums_tv.selection.selected_rows
2943
2944     call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2945                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
2946                  utf8(_("Please wait while scanning source directory...")),
2947                  'subdirs scan',
2948                  { :closure_after => proc {
2949                          open_file($filename)
2950                          $albums_tv.selection.select_path(sel[0])
2951                          $modified = true
2952                      } })
2953 end
2954
2955 def merge
2956     save_current_file
2957
2958     theme = $xmldoc.root.attributes['theme']
2959     limit_sizes = $xmldoc.root.attributes['limit-sizes']
2960     if limit_sizes
2961         limit_sizes = "--sizes #{limit_sizes}"
2962     end
2963     call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
2964                  "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
2965                  utf8(_("Please wait while scanning source directory...")),
2966                  'full scan',
2967                  { :closure_after => proc {
2968                          open_file($filename)
2969                          $modified = true
2970                      } })
2971 end
2972
2973 def save_as_do
2974     fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
2975                                     nil,
2976                                     Gtk::FileChooser::ACTION_SAVE,
2977                                     nil,
2978                                     [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2979     fc.transient_for = $main_window
2980     fc.add_shortcut_folder(File.expand_path("~/.booh"))
2981     fc.set_current_folder(File.expand_path("~/.booh"))
2982     fc.filename = $orig_filename
2983     if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2984         $orig_filename = fc.filename
2985         save_current_file_user
2986     end
2987     fc.destroy
2988 end
2989
2990 def preferences
2991     dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
2992                              $main_window,
2993                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2994                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2995                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2996
2997     dialog.vbox.add(notebook = Gtk::Notebook.new)
2998     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
2999     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3000                0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3001     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
3002                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3003     tooltips = Gtk::Tooltips.new
3004     tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3005 for example: /usr/bin/mplayer %f")), nil)
3006     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3007                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3008     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3009                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3010     tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3011 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3012     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3013                0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3014     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)),
3015                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3016     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)
3017     tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3018                0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3019     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)
3020     tbl.attach(emptycomments_check = Gtk::CheckButton.new(utf8(_("Use empty comments for new albums"))),
3021                0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3022     tooltips.set_tip(emptycomments_check, utf8(_("Normally, filenames are used as comments for new albums. Check this if you prefer empty comments.")), nil)
3023     tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3024                0, 2, 5, 6, Gtk::FILL, Gtk::SHRINK, 2, 2)
3025     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)
3026     smp_check.signal_connect('toggled') {
3027         if smp_check.active?
3028             smp_hbox.sensitive = true
3029         else
3030             smp_hbox.sensitive = false
3031         end
3032     }
3033     if $config['mproc']
3034         smp_check.active = true
3035         smp_spin.value = $config['mproc'].to_i
3036     end
3037     nogestures_check.active = $config['nogestures']
3038     emptycomments_check.active = $config['emptycomments']
3039     deleteondisk_check.active = $config['deleteondisk']
3040
3041     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3042     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3043                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3044     tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3045                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3046
3047     dialog.vbox.show_all
3048     dialog.run { |response|
3049         if response == Gtk::Dialog::RESPONSE_OK
3050             $config['video-viewer'] = video_viewer_entry.text
3051             $config['browser'] = browser_entry.text
3052             if smp_check.active?
3053                 $config['mproc'] = smp_spin.value.to_i
3054             else
3055                 $config.delete('mproc')
3056             end
3057             $config['nogestures'] = nogestures_check.active?
3058             $config['emptycomments'] = emptycomments_check.active?
3059             $config['deleteondisk'] = deleteondisk_check.active?
3060
3061             $config['convert-enhance'] = enhance_entry.text
3062         end
3063     }
3064     dialog.destroy
3065 end
3066
3067 def perform_undo
3068     if $undo_tb.sensitive?
3069         $redo_tb.sensitive = $redo_mb.sensitive = true
3070         if not more_undoes = UndoHandler.undo($statusbar)
3071             $undo_tb.sensitive = $undo_mb.sensitive = false
3072         end
3073     end
3074 end
3075
3076 def perform_redo
3077     if $redo_tb.sensitive?
3078         $undo_tb.sensitive = $undo_mb.sensitive = true
3079         if not more_redoes = UndoHandler.redo($statusbar)
3080             $redo_tb.sensitive = $redo_mb.sensitive = false
3081         end
3082     end
3083 end
3084
3085 def show_one_click_explanation(intro)
3086     show_popup($main_window, utf8(_("<b>One-Click tools.</b>
3087
3088 %s When such a tool is activated
3089 (<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
3090 on a thumbnail will immediately apply the desired action.
3091
3092 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
3093 ") % intro))
3094 end
3095
3096 def get_license
3097     return <<"EOF"
3098                     GNU GENERAL PUBLIC LICENSE
3099                        Version 2, June 1991
3100
3101  Copyright (C) 1989, 1991 Free Software Foundation, Inc.
3102                           675 Mass Ave, Cambridge, MA 02139, USA
3103  Everyone is permitted to copy and distribute verbatim copies
3104  of this license document, but changing it is not allowed.
3105
3106                             Preamble
3107
3108   The licenses for most software are designed to take away your
3109 freedom to share and change it.  By contrast, the GNU General Public
3110 License is intended to guarantee your freedom to share and change free
3111 software--to make sure the software is free for all its users.  This
3112 General Public License applies to most of the Free Software
3113 Foundation's software and to any other program whose authors commit to
3114 using it.  (Some other Free Software Foundation software is covered by
3115 the GNU Library General Public License instead.)  You can apply it to
3116 your programs, too.
3117
3118   When we speak of free software, we are referring to freedom, not
3119 price.  Our General Public Licenses are designed to make sure that you
3120 have the freedom to distribute copies of free software (and charge for
3121 this service if you wish), that you receive source code or can get it
3122 if you want it, that you can change the software or use pieces of it
3123 in new free programs; and that you know you can do these things.
3124
3125   To protect your rights, we need to make restrictions that forbid
3126 anyone to deny you these rights or to ask you to surrender the rights.
3127 These restrictions translate to certain responsibilities for you if you
3128 distribute copies of the software, or if you modify it.
3129
3130   For example, if you distribute copies of such a program, whether
3131 gratis or for a fee, you must give the recipients all the rights that
3132 you have.  You must make sure that they, too, receive or can get the
3133 source code.  And you must show them these terms so they know their
3134 rights.
3135
3136   We protect your rights with two steps: (1) copyright the software, and
3137 (2) offer you this license which gives you legal permission to copy,
3138 distribute and/or modify the software.
3139
3140   Also, for each author's protection and ours, we want to make certain
3141 that everyone understands that there is no warranty for this free
3142 software.  If the software is modified by someone else and passed on, we
3143 want its recipients to know that what they have is not the original, so
3144 that any problems introduced by others will not reflect on the original
3145 authors' reputations.
3146
3147   Finally, any free program is threatened constantly by software
3148 patents.  We wish to avoid the danger that redistributors of a free
3149 program will individually obtain patent licenses, in effect making the
3150 program proprietary.  To prevent this, we have made it clear that any
3151 patent must be licensed for everyone's free use or not licensed at all.
3152
3153   The precise terms and conditions for copying, distribution and
3154 modification follow.
3155
3156
3157                     GNU GENERAL PUBLIC LICENSE
3158    TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
3159
3160   0. This License applies to any program or other work which contains
3161 a notice placed by the copyright holder saying it may be distributed
3162 under the terms of this General Public License.  The "Program", below,
3163 refers to any such program or work, and a "work based on the Program"
3164 means either the Program or any derivative work under copyright law:
3165 that is to say, a work containing the Program or a portion of it,
3166 either verbatim or with modifications and/or translated into another
3167 language.  (Hereinafter, translation is included without limitation in
3168 the term "modification".)  Each licensee is addressed as "you".
3169
3170 Activities other than copying, distribution and modification are not
3171 covered by this License; they are outside its scope.  The act of
3172 running the Program is not restricted, and the output from the Program
3173 is covered only if its contents constitute a work based on the
3174 Program (independent of having been made by running the Program).
3175 Whether that is true depends on what the Program does.
3176
3177   1. You may copy and distribute verbatim copies of the Program's
3178 source code as you receive it, in any medium, provided that you
3179 conspicuously and appropriately publish on each copy an appropriate
3180 copyright notice and disclaimer of warranty; keep intact all the
3181 notices that refer to this License and to the absence of any warranty;
3182 and give any other recipients of the Program a copy of this License
3183 along with the Program.
3184
3185 You may charge a fee for the physical act of transferring a copy, and
3186 you may at your option offer warranty protection in exchange for a fee.
3187
3188   2. You may modify your copy or copies of the Program or any portion
3189 of it, thus forming a work based on the Program, and copy and
3190 distribute such modifications or work under the terms of Section 1
3191 above, provided that you also meet all of these conditions:
3192
3193     a) You must cause the modified files to carry prominent notices
3194     stating that you changed the files and the date of any change.
3195
3196     b) You must cause any work that you distribute or publish, that in
3197     whole or in part contains or is derived from the Program or any
3198     part thereof, to be licensed as a whole at no charge to all third
3199     parties under the terms of this License.
3200
3201     c) If the modified program normally reads commands interactively
3202     when run, you must cause it, when started running for such
3203     interactive use in the most ordinary way, to print or display an
3204     announcement including an appropriate copyright notice and a
3205     notice that there is no warranty (or else, saying that you provide
3206     a warranty) and that users may redistribute the program under
3207     these conditions, and telling the user how to view a copy of this
3208     License.  (Exception: if the Program itself is interactive but
3209     does not normally print such an announcement, your work based on
3210     the Program is not required to print an announcement.)
3211
3212
3213 These requirements apply to the modified work as a whole.  If
3214 identifiable sections of that work are not derived from the Program,
3215 and can be reasonably considered independent and separate works in
3216 themselves, then this License, and its terms, do not apply to those
3217 sections when you distribute them as separate works.  But when you
3218 distribute the same sections as part of a whole which is a work based
3219 on the Program, the distribution of the whole must be on the terms of
3220 this License, whose permissions for other licensees extend to the
3221 entire whole, and thus to each and every part regardless of who wrote it.
3222
3223 Thus, it is not the intent of this section to claim rights or contest
3224 your rights to work written entirely by you; rather, the intent is to
3225 exercise the right to control the distribution of derivative or
3226 collective works based on the Program.
3227
3228 In addition, mere aggregation of another work not based on the Program
3229 with the Program (or with a work based on the Program) on a volume of
3230 a storage or distribution medium does not bring the other work under
3231 the scope of this License.
3232
3233   3. You may copy and distribute the Program (or a work based on it,
3234 under Section 2) in object code or executable form under the terms of
3235 Sections 1 and 2 above provided that you also do one of the following:
3236
3237     a) Accompany it with the complete corresponding machine-readable
3238     source code, which must be distributed under the terms of Sections
3239     1 and 2 above on a medium customarily used for software interchange; or,
3240
3241     b) Accompany it with a written offer, valid for at least three
3242     years, to give any third party, for a charge no more than your
3243     cost of physically performing source distribution, a complete
3244     machine-readable copy of the corresponding source code, to be
3245     distributed under the terms of Sections 1 and 2 above on a medium
3246     customarily used for software interchange; or,
3247
3248     c) Accompany it with the information you received as to the offer
3249     to distribute corresponding source code.  (This alternative is
3250     allowed only for noncommercial distribution and only if you
3251     received the program in object code or executable form with such
3252     an offer, in accord with Subsection b above.)
3253
3254 The source code for a work means the preferred form of the work for
3255 making modifications to it.  For an executable work, complete source
3256 code means all the source code for all modules it contains, plus any
3257 associated interface definition files, plus the scripts used to
3258 control compilation and installation of the executable.  However, as a
3259 special exception, the source code distributed need not include
3260 anything that is normally distributed (in either source or binary
3261 form) with the major components (compiler, kernel, and so on) of the
3262 operating system on which the executable runs, unless that component
3263 itself accompanies the executable.
3264
3265 If distribution of executable or object code is made by offering
3266 access to copy from a designated place, then offering equivalent
3267 access to copy the source code from the same place counts as
3268 distribution of the source code, even though third parties are not
3269 compelled to copy the source along with the object code.
3270
3271
3272   4. You may not copy, modify, sublicense, or distribute the Program
3273 except as expressly provided under this License.  Any attempt
3274 otherwise to copy, modify, sublicense or distribute the Program is
3275 void, and will automatically terminate your rights under this License.
3276 However, parties who have received copies, or rights, from you under
3277 this License will not have their licenses terminated so long as such
3278 parties remain in full compliance.
3279
3280   5. You are not required to accept this License, since you have not
3281 signed it.  However, nothing else grants you permission to modify or
3282 distribute the Program or its derivative works.  These actions are
3283 prohibited by law if you do not accept this License.  Therefore, by
3284 modifying or distributing the Program (or any work based on the
3285 Program), you indicate your acceptance of this License to do so, and
3286 all its terms and conditions for copying, distributing or modifying
3287 the Program or works based on it.
3288
3289   6. Each time you redistribute the Program (or any work based on the
3290 Program), the recipient automatically receives a license from the
3291 original licensor to copy, distribute or modify the Program subject to
3292 these terms and conditions.  You may not impose any further
3293 restrictions on the recipients' exercise of the rights granted herein.
3294 You are not responsible for enforcing compliance by third parties to
3295 this License.
3296
3297   7. If, as a consequence of a court judgment or allegation of patent
3298 infringement or for any other reason (not limited to patent issues),
3299 conditions are imposed on you (whether by court order, agreement or
3300 otherwise) that contradict the conditions of this License, they do not
3301 excuse you from the conditions of this License.  If you cannot
3302 distribute so as to satisfy simultaneously your obligations under this
3303 License and any other pertinent obligations, then as a consequence you
3304 may not distribute the Program at all.  For example, if a patent
3305 license would not permit royalty-free redistribution of the Program by
3306 all those who receive copies directly or indirectly through you, then
3307 the only way you could satisfy both it and this License would be to
3308 refrain entirely from distribution of the Program.
3309
3310 If any portion of this section is held invalid or unenforceable under
3311 any particular circumstance, the balance of the section is intended to
3312 apply and the section as a whole is intended to apply in other
3313 circumstances.
3314
3315 It is not the purpose of this section to induce you to infringe any
3316 patents or other property right claims or to contest validity of any
3317 such claims; this section has the sole purpose of protecting the
3318 integrity of the free software distribution system, which is
3319 implemented by public license practices.  Many people have made
3320 generous contributions to the wide range of software distributed
3321 through that system in reliance on consistent application of that
3322 system; it is up to the author/donor to decide if he or she is willing
3323 to distribute software through any other system and a licensee cannot
3324 impose that choice.
3325
3326 This section is intended to make thoroughly clear what is believed to
3327 be a consequence of the rest of this License.
3328
3329
3330   8. If the distribution and/or use of the Program is restricted in
3331 certain countries either by patents or by copyrighted interfaces, the
3332 original copyright holder who places the Program under this License
3333 may add an explicit geographical distribution limitation excluding
3334 those countries, so that distribution is permitted only in or among
3335 countries not thus excluded.  In such case, this License incorporates
3336 the limitation as if written in the body of this License.
3337
3338   9. The Free Software Foundation may publish revised and/or new versions
3339 of the General Public License from time to time.  Such new versions will
3340 be similar in spirit to the present version, but may differ in detail to
3341 address new problems or concerns.
3342
3343 Each version is given a distinguishing version number.  If the Program
3344 specifies a version number of this License which applies to it and "any
3345 later version", you have the option of following the terms and conditions
3346 either of that version or of any later version published by the Free
3347 Software Foundation.  If the Program does not specify a version number of
3348 this License, you may choose any version ever published by the Free Software
3349 Foundation.
3350
3351   10. If you wish to incorporate parts of the Program into other free
3352 programs whose distribution conditions are different, write to the author
3353 to ask for permission.  For software which is copyrighted by the Free
3354 Software Foundation, write to the Free Software Foundation; we sometimes
3355 make exceptions for this.  Our decision will be guided by the two goals
3356 of preserving the free status of all derivatives of our free software and
3357 of promoting the sharing and reuse of software generally.
3358
3359                             NO WARRANTY
3360
3361   11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
3362 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
3363 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
3364 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
3365 OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
3366 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
3367 TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
3368 PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
3369 REPAIR OR CORRECTION.
3370
3371   12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
3372 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
3373 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
3374 INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
3375 OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
3376 TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
3377 YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
3378 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
3379 POSSIBILITY OF SUCH DAMAGES.
3380 EOF
3381 end
3382
3383 def create_menu_and_toolbar
3384     
3385     #- menu
3386     mb = Gtk::MenuBar.new
3387
3388     filemenu = Gtk::MenuItem.new(utf8(_("_File")))
3389     filesubmenu = Gtk::Menu.new
3390     filesubmenu.append(new       = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
3391     filesubmenu.append(open      = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
3392     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
3393     filesubmenu.append($save     = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
3394     filesubmenu.append($save_as  = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
3395     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
3396     tooltips = Gtk::Tooltips.new
3397     filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
3398     $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3399     tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
3400     filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
3401     $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3402     tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
3403     filesubmenu.append($merge    = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
3404     $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3405     tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums (anywhere)")), nil)
3406     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
3407     filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
3408     $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3409     tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
3410     filesubmenu.append($view_wa = Gtk::ImageMenuItem.new(utf8(_("View web-album with browser"))).set_sensitive(false))
3411     $view_wa.image = Gtk::Image.new("#{$FPATH}/images/stock-view-webalbum-16.png")
3412     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
3413     filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
3414     tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
3415     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
3416     filesubmenu.append(quit      = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
3417     filemenu.set_submenu(filesubmenu)
3418     mb.append(filemenu)
3419
3420     new.signal_connect('activate') { new_album }
3421     open.signal_connect('activate') { open_file_popup }
3422     $save.signal_connect('activate') { save_current_file_user }
3423     $save_as.signal_connect('activate') { save_as_do }
3424     $merge_current.signal_connect('activate') { merge_current }
3425     $merge_newsubs.signal_connect('activate') { merge_newsubs }
3426     $merge.signal_connect('activate') { merge }
3427     $generate.signal_connect('activate') {
3428         save_current_file
3429         call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
3430                      utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
3431                      'web-album',
3432                      { :successmsg => utf8(_("Your web-album is now ready in directory '%s'.
3433 Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
3434                        :successmsg_linkurl => $xmldoc.root.attributes['destination'],
3435                        :closure_after => proc {
3436                              $xmldoc.elements.each('//dir') { |elem|
3437                                  $modified ||= elem.attributes['already-generated'].nil?
3438                                  elem.add_attribute('already-generated', 'true')
3439                              }
3440                              UndoHandler.cleanup   #- prevent save_changes to mark current dir as not already generated
3441                              $undo_tb.sensitive = $undo_mb.sensitive = false
3442                              $redo_tb.sensitive = $redo_mb.sensitive = false
3443                              save_current_file
3444                              $generated_outofline = true
3445                          }})
3446     }
3447     $view_wa.signal_connect('activate') {
3448         indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
3449         if File.exists?(indexhtml)
3450             open_url(indexhtml)
3451         else
3452             show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
3453         end
3454     }
3455     $properties.signal_connect('activate') { properties }
3456
3457     quit.signal_connect('activate') { try_quit }
3458
3459     editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
3460     editsubmenu = Gtk::Menu.new
3461     editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
3462     editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
3463     editsubmenu.append(           Gtk::SeparatorMenuItem.new)
3464     editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
3465     $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
3466     tooltips.set_tip($remove_all_captions, utf8(_("Mainly useful when you don't want to type any caption, that will remove default captions made of filenames")), nil)
3467     editsubmenu.append(           Gtk::SeparatorMenuItem.new)
3468     editsubmenu.append(prefs    = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
3469     editmenu.set_submenu(editsubmenu)
3470     mb.append(editmenu)
3471
3472     $remove_all_captions.signal_connect('activate') { remove_all_captions }
3473
3474     prefs.signal_connect('activate') { preferences }
3475     
3476     helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
3477     helpsubmenu = Gtk::Menu.new
3478     helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
3479     one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
3480     helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
3481     speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
3482     helpsubmenu.append(tutos = Gtk::ImageMenuItem.new(utf8(_("Online tutorials (opens a web-browser)"))))
3483     tutos.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
3484     helpsubmenu.append(        Gtk::SeparatorMenuItem.new)
3485     helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
3486     helpmenu.set_submenu(helpsubmenu)
3487     mb.append(helpmenu)
3488
3489     one_click.signal_connect('activate') {
3490         show_one_click_explanation(_("One-Click tools are available in the toolbar."))
3491     }
3492     
3493     speed.signal_connect('activate') {
3494         show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
3495
3496 <span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
3497 <span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
3498 <span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
3499 <span foreground='darkblue'>Control-Enter</span>: for an image, open larger view; for a video, launch player
3500 <span foreground='darkblue'>Control-Delete</span>: delete image
3501 <span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
3502 <span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
3503 <span foreground='darkblue'>Control-z</span>: undo
3504 <span foreground='darkblue'>Control-r</span>: redo
3505
3506 <span size='large' weight='bold'>Mouse gestures:</span>
3507
3508 Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
3509 for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
3510
3511 <span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
3512 <span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
3513 <span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
3514 <span foreground='darkblue'>Left click, hold left button, right click</span>: undo
3515 <span foreground='darkblue'>Right click, hold right button, left click</span>: redo
3516 ")), { :pos_centered => true, :not_transient => true })
3517     }
3518
3519     tutos.signal_connect('activate') {
3520         open_url('http://zarb.org/~gc/html/booh/tuto