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