add "made with booh" by default on bottom of pages, suggested by coni
[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(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
875         refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
876         menu.append(delete_item  = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
877         delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
878     else
879         menu.append(               Gtk::SeparatorMenuItem.new)
880         menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
881         refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
882     end
883     menu.show_all
884     menu.popup(nil, nil, event.button, event.time)
885 end
886
887 def delete_current_subalbum
888     $modified = true
889     sel = $albums_tv.selection.selected_rows
890     $xmldir.elements.each { |e|
891         if e.name == 'image' || e.name == 'video'
892             e.add_attribute('deleted', 'true')
893         end
894     }
895     #- branch if we have a non deleted subalbum
896     if $xmldir.child_byname_notattr('dir', 'deleted')
897         $xmldir.delete_attribute('thumbnails-caption')
898         $xmldir.delete_attribute('thumbnails-captionfile')
899     else
900         $xmldir.add_attribute('deleted', 'true')
901         moveup = $xmldir
902         while moveup.parent.name == 'dir'
903             moveup = moveup.parent
904             if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
905                 moveup.add_attribute('deleted', 'true')
906             else
907                 break
908             end
909         end
910         sel[0].up!
911     end
912     save_changes('forced')
913     populate_subalbums_treeview(false)
914     $albums_tv.selection.select_path(sel[0])
915 end
916
917 def restore_deleted
918     $modified = true
919     save_changes
920     $current_path = nil  #- prevent save_changes from being rerun again
921     sel = $albums_tv.selection.selected_rows
922     restore_one = proc { |xmldir|
923         xmldir.elements.each { |e|
924             if e.name == 'dir' && e.attributes['deleted']
925                 restore_one.call(e)
926             end
927             e.delete_attribute('deleted')
928         }
929     }
930     restore_one.call($xmldir)
931     populate_subalbums_treeview(false)
932     $albums_tv.selection.select_path(sel[0])
933 end
934
935 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
936
937     img = nil
938     frame1 = Gtk::Frame.new
939     fullpath = from_utf8("#{$current_path}/#{filename}")
940
941     my_gen_real_thumbnail = proc {
942         gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
943     }
944
945     if type == 'video'
946         pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
947         frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
948                                  pack_start(img = Gtk::Image.new).
949                                  pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
950         px, mask = pxb.render_pixmap_and_mask
951         da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
952         da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
953     else
954         frame1.add(img = Gtk::Image.new)
955     end
956
957     #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
958     if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
959         my_gen_real_thumbnail.call
960     else
961         img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
962     end
963
964     evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
965
966     tooltips = Gtk::Tooltips.new
967     tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
968     tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
969
970     frame2, textview = create_editzone($autotable_sw, 1, img)
971     textview.buffer.text = caption
972     textview.set_justification(Gtk::Justification::CENTER)
973
974     vbox = Gtk::VBox.new(false, 5)
975     vbox.pack_start(evtbox, false, false)
976     vbox.pack_start(frame2, false, false)
977     autotable.append(vbox, filename)
978
979     #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
980     $vbox2widgets[vbox] = { :textview => textview, :image => img }
981
982     #- to be able to find widgets by name
983     $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
984
985     cleanup_all_thumbnails = proc {
986         #- remove out of sync images
987         dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
988         for sizeobj in $images_size
989             system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
990         end
991
992     }
993
994     refresh = proc {
995         cleanup_all_thumbnails.call
996         my_gen_real_thumbnail.call
997     }
998  
999     rotate_and_cleanup = proc { |angle|
1000         rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1001         cleanup_all_thumbnails.call
1002     }
1003
1004     move = proc { |direction|
1005         do_method = "move_#{direction}"
1006         undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1007         perform = proc {
1008             done = autotable.method(do_method).call(vbox)
1009             textview.grab_focus  #- because if moving, focus is stolen
1010             done
1011         }
1012         if perform.call
1013             save_undo(_("move %s") % direction,
1014                       proc {
1015                           autotable.method(undo_method).call(vbox)
1016                           textview.grab_focus  #- because if moving, focus is stolen
1017                           autoscroll_if_needed($autotable_sw, img, textview)
1018                           $notebook.set_page(1)
1019                           proc {
1020                               autotable.method(do_method).call(vbox)
1021                               textview.grab_focus  #- because if moving, focus is stolen
1022                               autoscroll_if_needed($autotable_sw, img, textview)
1023                               $notebook.set_page(1)
1024                           }
1025                       })
1026         end
1027     }
1028
1029     color_swap_and_cleanup = proc {
1030         perform_color_swap_and_cleanup = proc {
1031             color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1032             my_gen_real_thumbnail.call
1033         }
1034
1035         cleanup_all_thumbnails.call
1036         perform_color_swap_and_cleanup.call
1037
1038         save_undo(_("color swap"),
1039                   proc {
1040                       perform_color_swap_and_cleanup.call
1041                       textview.grab_focus
1042                       autoscroll_if_needed($autotable_sw, img, textview)
1043                       $notebook.set_page(1)
1044                       proc {
1045                           perform_color_swap_and_cleanup.call
1046                           textview.grab_focus
1047                           autoscroll_if_needed($autotable_sw, img, textview)
1048                           $notebook.set_page(1)
1049                       }
1050                   })
1051     }
1052
1053     change_frame_offset_and_cleanup_real = proc { |values|
1054         perform_change_frame_offset_and_cleanup = proc { |val|
1055             change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
1056             my_gen_real_thumbnail.call
1057         }
1058         perform_change_frame_offset_and_cleanup.call(values[:new])
1059         
1060         save_undo(_("specify frame offset"),
1061                   proc {
1062                       perform_change_frame_offset_and_cleanup.call(values[:old])
1063                       textview.grab_focus
1064                       autoscroll_if_needed($autotable_sw, img, textview)
1065                       $notebook.set_page(1)
1066                       proc {
1067                           perform_change_frame_offset_and_cleanup.call(values[:new])
1068                           textview.grab_focus
1069                           autoscroll_if_needed($autotable_sw, img, textview)
1070                           $notebook.set_page(1)
1071                       }
1072                   })
1073     }
1074
1075     change_frame_offset_and_cleanup = proc {
1076         if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
1077             change_frame_offset_and_cleanup_real.call(values)
1078         end
1079     }
1080
1081     change_pano_amount_and_cleanup_real = proc { |values|
1082         perform_change_pano_amount_and_cleanup = proc { |val|
1083             change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1084         }
1085         perform_change_pano_amount_and_cleanup.call(values[:new])
1086         
1087         save_undo(_("change panorama amount"),
1088                   proc {
1089                       perform_change_pano_amount_and_cleanup.call(values[:old])
1090                       textview.grab_focus
1091                       autoscroll_if_needed($autotable_sw, img, textview)
1092                       $notebook.set_page(1)
1093                       proc {
1094                           perform_change_pano_amount_and_cleanup.call(values[:new])
1095                           textview.grab_focus
1096                           autoscroll_if_needed($autotable_sw, img, textview)
1097                           $notebook.set_page(1)
1098                       }
1099                   })
1100     }
1101
1102     change_pano_amount_and_cleanup = proc {
1103         if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1104             change_pano_amount_and_cleanup_real.call(values)
1105         end
1106     }
1107
1108     whitebalance_and_cleanup_real = proc { |values|
1109         perform_change_whitebalance_and_cleanup = proc { |val|
1110             change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1111             recalc_whitebalance(val, fullpath, thumbnail_img, img,
1112                                 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1113             cleanup_all_thumbnails.call
1114         }
1115         perform_change_whitebalance_and_cleanup.call(values[:new])
1116
1117         save_undo(_("fix white balance"),
1118                   proc {
1119                       perform_change_whitebalance_and_cleanup.call(values[:old])
1120                       textview.grab_focus
1121                       autoscroll_if_needed($autotable_sw, img, textview)
1122                       $notebook.set_page(1)
1123                       proc {
1124                           perform_change_whitebalance_and_cleanup.call(values[:new])
1125                           textview.grab_focus
1126                           autoscroll_if_needed($autotable_sw, img, textview)
1127                           $notebook.set_page(1)
1128                       }
1129                   })
1130     }
1131
1132     whitebalance_and_cleanup = proc {
1133         if values = ask_whitebalance(fullpath, thumbnail_img, img,
1134                                      $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1135             whitebalance_and_cleanup_real.call(values)
1136         end
1137     }
1138
1139     enhance_and_cleanup = proc {
1140         perform_enhance_and_cleanup = proc {
1141             enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1142             my_gen_real_thumbnail.call
1143         }
1144
1145         cleanup_all_thumbnails.call
1146         perform_enhance_and_cleanup.call
1147
1148         save_undo(_("enhance"),
1149                   proc {
1150                       perform_enhance_and_cleanup.call
1151                       textview.grab_focus
1152                       autoscroll_if_needed($autotable_sw, img, textview)
1153                       $notebook.set_page(1)
1154                       proc {
1155                           perform_enhance_and_cleanup.call
1156                           textview.grab_focus
1157                           autoscroll_if_needed($autotable_sw, img, textview)
1158                           $notebook.set_page(1)
1159                       }
1160                   })
1161     }
1162
1163     delete = proc { |isacut|
1164         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 })
1165             $modified = true
1166             after = nil
1167             perform_delete = proc {
1168                 after = autotable.get_next_widget(vbox)
1169                 if !after
1170                     after = autotable.get_previous_widget(vbox)
1171                 end
1172                 if $config['deleteondisk'] && !isacut
1173                     msg 3, "scheduling for delete: #{fullpath}"
1174                     $todelete << fullpath
1175                 end
1176                 autotable.remove(vbox)
1177                 if after
1178                     $vbox2widgets[after][:textview].grab_focus
1179                     autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1180                 end
1181             }
1182             
1183             previous_pos = autotable.get_current_number(vbox)
1184             perform_delete.call
1185
1186             if !after
1187                 delete_current_subalbum
1188             else
1189                 save_undo(_("delete"),
1190                           proc { |pos|
1191                               autotable.reinsert(pos, vbox, filename)
1192                               $notebook.set_page(1)
1193                               autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1194                               $cuts = []
1195                               msg 3, "removing deletion schedule of: #{fullpath}"
1196                               $todelete.delete(fullpath)  #- unconditional because deleteondisk option could have been modified
1197                               proc {
1198                                   perform_delete.call
1199                                   $notebook.set_page(1)
1200                               }
1201                           }, previous_pos)
1202             end
1203         end
1204     }
1205
1206     cut = proc {
1207         delete.call(true)
1208         $cuts << { :vbox => vbox, :filename => filename }
1209         $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1210     }
1211     paste = proc {
1212         if $cuts.size > 0
1213             $cuts.each { |elem|
1214                 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1215             }
1216             last = $cuts[-1]
1217             autotable.queue_draws << proc {
1218                 $vbox2widgets[last[:vbox]][:textview].grab_focus
1219                 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1220             }
1221             save_undo(_("paste"),
1222                       proc { |cuts|
1223                           cuts.each { |elem| autotable.remove(elem[:vbox]) }
1224                           $notebook.set_page(1)
1225                           proc {
1226                               cuts.each { |elem|
1227                                   autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1228                               }
1229                               $notebook.set_page(1)
1230                           }
1231                       }, $cuts)
1232             $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1233             $cuts = []
1234         end
1235     }
1236
1237     $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1238                                  :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1239                                  :whitebalance => whitebalance_and_cleanup_real, :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1240
1241     textview.signal_connect('key-press-event') { |w, event|
1242         propagate = true
1243         if event.state != 0
1244             x, y = autotable.get_current_pos(vbox)
1245             control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1246             shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1247             alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1248             if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1249                 if control_pressed
1250                     if widget_up = autotable.get_widget_at_pos(x, y - 1)
1251                         $vbox2widgets[widget_up][:textview].grab_focus
1252                     end
1253                 end
1254                 if shift_pressed
1255                     move.call('up')
1256                 end
1257             end
1258             if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1259                 if control_pressed
1260                     if widget_down = autotable.get_widget_at_pos(x, y + 1)
1261                         $vbox2widgets[widget_down][:textview].grab_focus
1262                     end
1263                 end
1264                 if shift_pressed
1265                     move.call('down')
1266                 end
1267             end
1268             if event.keyval == Gdk::Keyval::GDK_Left
1269                 if x > 0
1270                     if control_pressed
1271                         $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1272                     end
1273                     if shift_pressed
1274                         move.call('left')
1275                     end
1276                 end
1277                 if alt_pressed
1278                     rotate_and_cleanup.call(-90)
1279                 end
1280             end
1281             if event.keyval == Gdk::Keyval::GDK_Right
1282                 next_ = autotable.get_next_widget(vbox)
1283                 if next_ && autotable.get_current_pos(next_)[0] > x
1284                     if control_pressed
1285                         $vbox2widgets[next_][:textview].grab_focus
1286                     end
1287                     if shift_pressed
1288                         move.call('right')
1289                     end
1290                 end
1291                 if alt_pressed
1292                     rotate_and_cleanup.call(90)
1293                 end
1294             end
1295             if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1296                 delete.call(false)
1297             end
1298             if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1299                 view_element(filename, { :delete => delete })
1300                 propagate = false
1301             end
1302             if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1303                 perform_undo
1304             end
1305             if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1306                 perform_redo
1307             end
1308         end
1309         !propagate  #- propagate if needed
1310     }
1311
1312     $ignore_next_release = false
1313     evtbox.signal_connect('button-press-event') { |w, event|
1314         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1315             if event.state & Gdk::Window::BUTTON3_MASK != 0
1316                 #- gesture redo: hold right mouse button then click left mouse button
1317                 $config['nogestures'] or perform_redo
1318                 $ignore_next_release = true
1319             else
1320                 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1321                 if $r90.active?
1322                     rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1323                 elsif $r270.active?
1324                     rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1325                 elsif $enhance.active?
1326                     enhance_and_cleanup.call
1327                 elsif $delete.active?
1328                     delete.call(false)
1329                 else
1330                     textview.grab_focus
1331                     $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1332                 end
1333             end
1334             $button1_pressed_autotable = true
1335         elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1336             if event.state & Gdk::Window::BUTTON1_MASK != 0
1337                 #- gesture undo: hold left mouse button then click right mouse button
1338                 $config['nogestures'] or perform_undo
1339                 $ignore_next_release = true
1340             end
1341         elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1342             view_element(filename, { :delete => delete })
1343         end
1344         false   #- propagate
1345     }
1346
1347     evtbox.signal_connect('button-release-event') { |w, event|
1348         if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1349             if !$ignore_next_release
1350                 x, y = autotable.get_current_pos(vbox)
1351                 next_ = autotable.get_next_widget(vbox)
1352                 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1353                                      { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1354                                        :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1355                                      { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1356                                        :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1357                                        :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1358                                        :pano => change_pano_amount_and_cleanup, :refresh => refresh })
1359             end
1360             $ignore_next_release = false
1361             $gesture_press = nil
1362         end
1363         false   #- propagate
1364     }
1365
1366     #- handle reordering with drag and drop
1367     Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1368     Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1369     vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1370         selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1371     }
1372
1373     vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1374         done = false
1375         #- mouse gesture first (dnd disables button-release-event)
1376         if $gesture_press && $gesture_press[:filename] == filename
1377             if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1378                 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1379                 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1380                 rotate_and_cleanup.call(angle)
1381                 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1382                 done = true
1383             elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1384                 msg 3, "gesture delete: click-drag right button to the bottom"
1385                 delete.call(false)
1386                 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1387                 done = true
1388             end
1389         end
1390         if !done
1391             ctxt.targets.each { |target|
1392                 if target.name == 'reorder-elements'
1393                     move_dnd = proc { |from,to|
1394                         if from != to
1395                             $modified = true
1396                             autotable.move(from, to)
1397                             save_undo(_("reorder"),
1398                                       proc { |from, to|
1399                                           if to > from
1400                                               autotable.move(to - 1, from)
1401                                           else
1402                                               autotable.move(to, from + 1)
1403                                           end
1404                                           $notebook.set_page(1)
1405                                           proc {
1406                                               autotable.move(from, to)
1407                                               $notebook.set_page(1)
1408                                           }
1409                                       }, from, to)
1410                         end
1411                     }
1412                     if $multiple_dnd.size == 0
1413                         move_dnd.call(selection_data.data.to_i,
1414                                       autotable.get_current_number(vbox))
1415                     else
1416                         UndoHandler.begin_batch
1417                         $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1418                                       each { |path|
1419                             #- need to update current position between each call
1420                             move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1421                                           autotable.get_current_number(vbox))
1422                         }
1423                         UndoHandler.end_batch
1424                     end
1425                     $multiple_dnd = []
1426                 end
1427             }
1428         end
1429     }
1430
1431     vbox.show_all
1432 end
1433
1434 def create_auto_table
1435
1436     $autotable = Gtk::AutoTable.new(5)
1437
1438     $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1439     thumbnails_vb = Gtk::VBox.new(false, 5)
1440
1441     frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1442     $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1443     thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1444     thumbnails_vb.add($autotable)
1445
1446     $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1447     $autotable_sw.add_with_viewport(thumbnails_vb)
1448
1449     #- follows stuff for handling multiple elements selection
1450     press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1451     gc = nil
1452     update_selected = proc {
1453         $autotable.current_order.each { |path|
1454             w = $name2widgets[path][:evtbox].window
1455             xm = w.position[0] + w.size[0]/2
1456             ym = w.position[1] + w.size[1]/2
1457             if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1458                 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1459                     $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1460                     $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1461                 end
1462             end
1463             if $selected_elements[path] && ! $selected_elements[path][:keep]
1464                 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))
1465                     $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1466                     $selected_elements.delete(path)
1467                 end
1468             end
1469         }
1470     }
1471     $autotable.signal_connect('realize') { |w,e|
1472         gc = Gdk::GC.new($autotable.window)
1473         gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1474         gc.function = Gdk::GC::INVERT
1475         #- autoscroll handling for DND and multiple selections
1476         Gtk.timeout_add(100) {
1477             if ! $autotable.window.nil?
1478                 w, x, y, mask = $autotable.window.pointer
1479                 if mask & Gdk::Window::BUTTON1_MASK != 0
1480                     if y < $autotable_sw.vadjustment.value
1481                         if pos_x
1482                             $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]])
1483                         end
1484                         if $button1_pressed_autotable || press_x
1485                             scroll_upper($autotable_sw, y)
1486                         end
1487                         if not press_x.nil?
1488                             w, pos_x, pos_y = $autotable.window.pointer
1489                             $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]])
1490                             update_selected.call
1491                         end
1492                     end
1493                     if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1494                         if pos_x
1495                             $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]])
1496                         end
1497                         if $button1_pressed_autotable || press_x
1498                             scroll_lower($autotable_sw, y)
1499                         end
1500                         if not press_x.nil?
1501                             w, pos_x, pos_y = $autotable.window.pointer
1502                             $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]])
1503                             update_selected.call
1504                         end
1505                     end
1506                 end
1507             end
1508             ! $autotable.window.nil?
1509         }
1510     }
1511
1512     $autotable.signal_connect('button-press-event') { |w,e|
1513         if e.button == 1
1514             if !$button1_pressed_autotable
1515                 press_x = e.x
1516                 press_y = e.y
1517                 if e.state & Gdk::Window::SHIFT_MASK == 0
1518                     $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1519                     $selected_elements = {}
1520                     $statusbar.push(0, utf8(_("Nothing selected.")))
1521                 else
1522                     $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1523                 end
1524                 set_mousecursor(Gdk::Cursor::TCROSS)
1525             end
1526         end
1527     }
1528     $autotable.signal_connect('button-release-event') { |w,e|
1529         if e.button == 1
1530             if $button1_pressed_autotable
1531                 #- unselect all only now
1532                 $multiple_dnd = $selected_elements.keys
1533                 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1534                 $selected_elements = {}
1535                 $button1_pressed_autotable = false
1536             else
1537                 if pos_x
1538                     $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]])
1539                     if $selected_elements.length > 0
1540                         $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1541                     end
1542                 end
1543                 press_x = press_y = pos_x = pos_y = nil
1544                 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1545             end
1546         end
1547     }
1548     $autotable.signal_connect('motion-notify-event') { |w,e|
1549         if ! press_x.nil?
1550             if pos_x
1551                 $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]])
1552             end
1553             pos_x = e.x
1554             pos_y = e.y
1555             $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]])
1556             update_selected.call
1557         end
1558     }
1559
1560 end
1561
1562 def create_subalbums_page
1563
1564     subalbums_hb = Gtk::HBox.new
1565     $subalbums_vb = Gtk::VBox.new(false, 5)
1566     subalbums_hb.pack_start($subalbums_vb, false, false)
1567     $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1568     $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1569     $subalbums_sw.add_with_viewport(subalbums_hb)
1570 end
1571
1572 def save_current_file
1573     save_changes
1574
1575     if $filename
1576         begin
1577             begin
1578                 ios = File.open($filename, "w")
1579                 $xmldoc.write(ios, 0)
1580                 ios.close
1581             rescue Iconv::IllegalSequence
1582                 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1583                 if ! ios.nil? && ! ios.closed?
1584                     ios.close
1585                 end
1586                 $xmldoc.xml_decl.encoding = 'UTF-8'
1587                 ios = File.open($filename, "w")
1588                 $xmldoc.write(ios, 0)
1589                 ios.close
1590             end
1591             return true
1592         rescue Exception
1593             return false
1594         end
1595     end
1596 end
1597
1598 def save_current_file_user
1599     save_tempfilename = $filename
1600     $filename = $orig_filename
1601     if ! save_current_file
1602         show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1603         $filename = save_tempfilename
1604         return
1605     end
1606     $modified = false
1607     $generated_outofline = false
1608     $filename = save_tempfilename
1609
1610     msg 3, "performing actual deletion of: " + $todelete.join(', ')
1611     $todelete.each { |f|
1612         system("rm -f #{f}")
1613     }
1614 end
1615
1616 def mark_document_as_dirty
1617     $xmldoc.elements.each('//dir') { |elem|
1618         elem.delete_attribute('already-generated')
1619     }
1620 end
1621
1622 #- ret: true => ok  false => cancel
1623 def ask_save_modifications(msg1, msg2, *options)
1624     ret = true
1625     options = options.size > 0 ? options[0] : {}
1626     if $modified
1627         if options[:disallow_cancel]
1628             dialog = Gtk::Dialog.new(msg1,
1629                                      $main_window,
1630                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1631                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1632                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1633         else
1634             dialog = Gtk::Dialog.new(msg1,
1635                                      $main_window,
1636                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1637                                      [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1638                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1639                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1640         end
1641         dialog.default_response = Gtk::Dialog::RESPONSE_YES
1642         dialog.vbox.add(Gtk::Label.new(msg2))
1643         dialog.window_position = Gtk::Window::POS_CENTER
1644         dialog.show_all
1645         
1646         dialog.run { |response|
1647             dialog.destroy
1648             if response == Gtk::Dialog::RESPONSE_YES
1649                 if ! save_current_file_user
1650                     return ask_save_modifications(msg1, msg2, options)
1651                 end
1652             else
1653                 #- if we have generated an album but won't save modifications, we must remove 
1654                 #- already-generated markers in original file
1655                 if $generated_outofline
1656                     begin
1657                         $xmldoc = REXML::Document.new File.new($orig_filename)
1658                         mark_document_as_dirty
1659                         ios = File.open($orig_filename, "w")
1660                         $xmldoc.write(ios, 0)
1661                         ios.close
1662                     rescue Exception
1663                         puts "exception: #{$!}"
1664                     end
1665                 end
1666             end
1667             if response == Gtk::Dialog::RESPONSE_CANCEL
1668                 ret = false
1669             end
1670             $todelete = []  #- unconditionally clear the list of images/videos to delete
1671         }
1672     end
1673     return ret
1674 end
1675
1676 def try_quit(*options)
1677     if ask_save_modifications(utf8(_("Save before quitting?")),
1678                               utf8(_("Do you want to save your changes before quitting?")),
1679                               *options)
1680         Gtk.main_quit
1681     end
1682 end
1683
1684 def show_popup(parent, msg, *options)
1685     dialog = Gtk::Dialog.new
1686     if options[0] && options[0][:title]
1687         dialog.title = options[0][:title]
1688     else
1689         dialog.title = utf8(_("Booh message"))
1690     end
1691     lbl = Gtk::Label.new
1692     if options[0] && options[0][:nomarkup]
1693         lbl.text = msg
1694     else
1695         lbl.markup = msg
1696     end
1697     if options[0] && options[0][:centered]
1698         lbl.set_justify(Gtk::Justification::CENTER)
1699     end
1700     if options[0] && options[0][:selectable]
1701         lbl.selectable = true
1702     end
1703     if options[0] && options[0][:topwidget]
1704         dialog.vbox.add(options[0][:topwidget])
1705     end
1706     if options[0] && options[0][:scrolled]
1707         sw = Gtk::ScrolledWindow.new(nil, nil)
1708         sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1709         sw.add_with_viewport(lbl)
1710         dialog.vbox.add(sw)
1711         dialog.set_default_size(500, 600)
1712     else
1713         dialog.vbox.add(lbl)
1714         dialog.set_default_size(200, 120)
1715     end
1716     if options[0] && options[0][:okcancel]
1717         dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1718     end
1719     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1720
1721     if options[0] && options[0][:pos_centered]
1722         dialog.window_position = Gtk::Window::POS_CENTER
1723     else
1724         dialog.window_position = Gtk::Window::POS_MOUSE
1725     end
1726
1727     if options[0] && options[0][:linkurl]
1728         linkbut = Gtk::Button.new('')
1729         linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1730         linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1731         linkbut.relief = Gtk::RELIEF_NONE
1732         linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1733         linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1734         dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1735     end
1736
1737     dialog.show_all
1738
1739     if !options[0] || !options[0][:not_transient]
1740         dialog.transient_for = parent
1741         dialog.run { |response|
1742             dialog.destroy
1743             if options[0] && options[0][:okcancel]
1744                 return response == Gtk::Dialog::RESPONSE_OK
1745             end
1746         }
1747     else
1748         dialog.signal_connect('response') { dialog.destroy }
1749     end
1750 end
1751
1752 def backend_wait_message(parent, msg, infopipe_path, mode)
1753     w = Gtk::Window.new
1754     w.set_transient_for(parent)
1755     w.modal = true
1756
1757     vb = Gtk::VBox.new(false, 5).set_border_width(5)
1758     vb.pack_start(Gtk::Label.new(msg), false, false)
1759
1760     vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1761     vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1762     if mode != 'one dir scan'
1763         vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1764     end
1765     if mode == 'web-album'
1766         vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1767         vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1768     end
1769     vb.pack_start(Gtk::HSeparator.new, false, false)
1770
1771     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1772     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1773     vb.pack_end(bottom, false, false)
1774
1775     infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1776     refresh_thread = Thread.new {
1777         directories_counter = 0
1778         while line = infopipe.gets
1779             if line =~ /^directories: (\d+), sizes: (\d+)/
1780                 directories = $1.to_f + 1
1781                 sizes = $2.to_f
1782             elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1783                 elements = $3.to_f + 1
1784                 if mode == 'web-album'
1785                     elements += sizes
1786                 end
1787                 element_counter = 0
1788                 gtk_thread_protect { pb1_1.fraction = 0 }
1789                 if mode != 'one dir scan'
1790                     newtext = utf8(full_src_dir_to_rel($1, $2))
1791                     newtext = '/' if newtext == ''
1792                     gtk_thread_protect { pb1_2.text = newtext }
1793                     directories_counter += 1
1794                     gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1795                 end
1796             elsif line =~ /^processing element$/
1797                 element_counter += 1
1798                 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1799             elsif line =~ /^processing size$/
1800                 element_counter += 1
1801                 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1802             elsif line =~ /^finished processing sizes$/
1803                 gtk_thread_protect { pb1_1.fraction = 1 }
1804             elsif line =~ /^creating index.html$/
1805                 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1806                 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1807                 directories_counter = 0
1808             elsif line =~ /^index.html: (.+)\|(.+)/
1809                 newtext = utf8(full_src_dir_to_rel($1, $2))
1810                 newtext = '/' if newtext == ''
1811                 gtk_thread_protect { pb2.text = newtext }
1812                 directories_counter += 1
1813                 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1814             elsif line =~ /^die: (.*)$/
1815                 $diemsg = $1
1816             end
1817         end
1818     }
1819
1820     w.add(vb)
1821     w.signal_connect('delete-event') { w.destroy }
1822     w.signal_connect('destroy') {
1823         Thread.kill(refresh_thread)
1824         gtk_thread_flush  #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1825         if infopipe_path
1826             infopipe.close
1827             system("rm -f #{infopipe_path}")
1828         end
1829     }
1830     w.window_position = Gtk::Window::POS_CENTER
1831     w.show_all
1832
1833     return [ b, w ]
1834 end
1835
1836 def call_backend(cmd, waitmsg, mode, params)
1837     pipe = Tempfile.new("boohpipe")
1838     pipe.close!
1839     system("mkfifo #{pipe.path}")
1840     cmd += " --info-pipe #{pipe.path}"
1841     button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
1842     pid = nil
1843     Thread.new {
1844         msg 2, cmd
1845         if pid = fork
1846             id, exitstatus = Process.waitpid2(pid)
1847             gtk_thread_protect { w8.destroy }
1848             if exitstatus == 0
1849                 if params[:successmsg]
1850                     gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
1851                 end
1852                 if params[:closure_after]
1853                     gtk_thread_protect(&params[:closure_after])
1854                 end
1855             elsif exitstatus == 15
1856                 #- say nothing, user aborted
1857             else
1858                 gtk_thread_protect { show_popup($main_window,
1859                                                 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
1860             end
1861         else
1862             exec(cmd)
1863         end
1864     }
1865     button.signal_connect('clicked') {
1866         Process.kill('SIGTERM', pid)
1867     }
1868 end
1869
1870 def save_changes(*forced)
1871     if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1872         return
1873     end
1874
1875     $xmldir.delete_attribute('already-generated')
1876
1877     propagate_children = proc { |xmldir|
1878         if xmldir.attributes['subdirs-caption']
1879             xmldir.delete_attribute('already-generated')
1880         end
1881         xmldir.elements.each('dir') { |element|
1882             propagate_children.call(element)
1883         }
1884     }
1885
1886     if $xmldir.child_byname_notattr('dir', 'deleted')
1887         new_title = $subalbums_title.buffer.text
1888         if new_title != $xmldir.attributes['subdirs-caption']
1889             parent = $xmldir.parent
1890             if parent.name == 'dir'
1891                 parent.delete_attribute('already-generated')
1892             end
1893             propagate_children.call($xmldir)
1894         end
1895         $xmldir.add_attribute('subdirs-caption', new_title)
1896         $xmldir.elements.each('dir') { |element|
1897             if !element.attributes['deleted']
1898                 path = element.attributes['path']
1899                 newtext = $subalbums_edits[path][:editzone].buffer.text
1900                 if element.attributes['subdirs-caption']
1901                     if element.attributes['subdirs-caption'] != newtext
1902                         propagate_children.call(element)
1903                     end
1904                     element.add_attribute('subdirs-caption',     newtext)
1905                     element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1906                 else
1907                     if element.attributes['thumbnails-caption'] != newtext
1908                         element.delete_attribute('already-generated')
1909                     end
1910                     element.add_attribute('thumbnails-caption',     newtext)
1911                     element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1912                 end
1913             end
1914         }
1915     end
1916
1917     if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
1918         if $xmldir.attributes['thumbnails-caption']
1919             path = $xmldir.attributes['path']
1920             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1921         end
1922     elsif $xmldir.attributes['thumbnails-caption']
1923         $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
1924     end
1925
1926     #- remove and reinsert elements to reflect new ordering
1927     saves = {}
1928     cpt = 0
1929     $xmldir.elements.each { |element|
1930         if element.name == 'image' || element.name == 'video'
1931             saves[element.attributes['filename']] = element.remove
1932             cpt += 1
1933         end
1934     }
1935     $autotable.current_order.each { |path|
1936         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1937         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1938         saves.delete(path)
1939     }
1940     saves.each_key { |path|
1941         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
1942         chld.add_attribute('deleted', 'true')
1943     }
1944 end
1945
1946 def sort_by_exif_date
1947     $modified = true
1948     save_changes
1949     current_order = []
1950     $xmldir.elements.each { |element|
1951         if element.name == 'image' || element.name == 'video'
1952             current_order << element.attributes['filename']
1953         end
1954     }
1955
1956     #- look for EXIF dates
1957     w = Gtk::Window.new
1958     w.set_transient_for($main_window)
1959     w.modal = true
1960     vb = Gtk::VBox.new(false, 5).set_border_width(5)
1961     vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
1962     vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
1963     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1964     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1965     vb.pack_end(bottom, false, false)
1966     w.add(vb)
1967     w.signal_connect('delete-event') { w.destroy }
1968     w.window_position = Gtk::Window::POS_CENTER
1969     w.show_all
1970
1971     aborted = false
1972     b.signal_connect('clicked') { aborted = true }
1973     dates = {}
1974     i = 0
1975     current_order.each { |f|
1976         i += 1
1977         if entry2type(f) == 'image'
1978             pb.text = f
1979             pb.fraction = i.to_f / current_order.size
1980             Gtk.main_iteration while Gtk.events_pending?
1981             date_time = `identify -format "%[EXIF:DateTime]" '#{from_utf8($current_path + "/" + f)}'`.chomp
1982             if $? == 0 && date_time != ''
1983                 dates[f] = date_time
1984             end
1985         end
1986         if aborted
1987             break
1988         end
1989     }
1990     w.destroy
1991     if aborted
1992         return
1993     end
1994
1995     saves = {}
1996     $xmldir.elements.each { |element|
1997         if element.name == 'image' || element.name == 'video'
1998             saves[element.attributes['filename']] = element.remove
1999         end
2000     }
2001
2002     #- find a good fallback for all entries without a date (still next to the item they were next to)
2003     neworder = dates.keys.sort { |a,b| dates[a] <=> dates[b] }
2004     for i in 0 .. current_order.size - 1
2005         if ! neworder.include?(current_order[i])
2006             j = i - 1
2007             while j > 0 && ! neworder.include?(current_order[j])
2008                 j -= 1
2009             end
2010             neworder[(neworder.index(current_order[j]) || -1 ) + 1, 0] = current_order[i]
2011         end
2012     end
2013     neworder.each { |f|
2014         $xmldir.add_element(saves[f].name, saves[f].attributes)
2015     }
2016
2017     #- let the auto-table reflect new ordering
2018     change_dir
2019 end
2020
2021 def remove_all_captions
2022     $modified = true
2023     texts = {}
2024     $autotable.current_order.each { |path|
2025         texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2026         $name2widgets[File.basename(path)][:textview].buffer.text = ''
2027     }
2028     save_undo(_("remove all captions"),
2029               proc { |texts|
2030                   texts.each_key { |key|
2031                       $name2widgets[key][:textview].buffer.text = texts[key]
2032                   }
2033                   $notebook.set_page(1)
2034                   proc {
2035                       texts.each_key { |key|
2036                           $name2widgets[key][:textview].buffer.text = ''
2037                       }
2038                       $notebook.set_page(1)
2039                   }
2040               }, texts)
2041 end
2042
2043 def change_dir
2044     $selected_elements.each_key { |path|
2045         $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2046     }
2047     $autotable.clear
2048     $vbox2widgets = {}
2049     $name2widgets = {}
2050     $name2closures = {}
2051     $selected_elements = {}
2052     $cuts = []
2053     $multiple_dnd = []
2054     UndoHandler.cleanup
2055     $undo_tb.sensitive = $undo_mb.sensitive = false
2056     $redo_tb.sensitive = $redo_mb.sensitive = false
2057
2058     if !$current_path
2059         return
2060     end
2061
2062     $subalbums_vb.children.each { |chld|
2063         $subalbums_vb.remove(chld)
2064     }
2065     $subalbums = Gtk::Table.new(0, 0, true)
2066     current_y_sub_albums = 0
2067
2068     $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2069     $subalbums_edits = {}
2070     subalbums_counter = 0
2071     subalbums_edits_bypos = {}
2072
2073     add_subalbum = proc { |xmldir, counter|
2074         $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2075         subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2076         if xmldir == $xmldir
2077             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2078             caption = xmldir.attributes['thumbnails-caption']
2079             captionfile, dummy = find_subalbum_caption_info(xmldir)
2080             infotype = 'thumbnails'
2081         else
2082             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2083             captionfile, caption = find_subalbum_caption_info(xmldir)
2084             infotype = find_subalbum_info_type(xmldir)
2085         end
2086         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2087         hbox = Gtk::HBox.new
2088         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2089         f = Gtk::Frame.new
2090         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2091
2092         img = nil
2093         my_gen_real_thumbnail = proc {
2094             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2095         }
2096
2097         if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2098             f.add(img = Gtk::Image.new)
2099             my_gen_real_thumbnail.call
2100         else
2101             f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2102         end
2103         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2104         $subalbums.attach(hbox,
2105                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2106
2107         frame, textview = create_editzone($subalbums_sw, 0, img)
2108         textview.buffer.text = caption
2109         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2110                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2111
2112         change_image = proc {
2113             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2114                                             nil,
2115                                             Gtk::FileChooser::ACTION_OPEN,
2116                                             nil,
2117                                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2118             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2119             fc.transient_for = $main_window
2120             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))
2121             f.add(preview_img = Gtk::Image.new)
2122             preview.show_all
2123             fc.signal_connect('update-preview') { |w|
2124                 begin
2125                     if fc.preview_filename
2126                         preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2127                         fc.preview_widget_active = true
2128                     end
2129                 rescue Gdk::PixbufError
2130                     fc.preview_widget_active = false
2131                 end
2132             }
2133             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2134                 $modified = true
2135                 old_file = captionfile
2136                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2137                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2138                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2139                 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2140
2141                 new_file = fc.filename
2142                 msg 3, "new captionfile is: #{fc.filename}"
2143                 perform_changefile = proc {
2144                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2145                     $modified_pixbufs.delete(thumbnail_file)
2146                     xmldir.delete_attribute("#{infotype}-rotate")
2147                     xmldir.delete_attribute("#{infotype}-color-swap")
2148                     xmldir.delete_attribute("#{infotype}-enhance")
2149                     xmldir.delete_attribute("#{infotype}-frame-offset")
2150                     my_gen_real_thumbnail.call
2151                 }
2152                 perform_changefile.call
2153
2154                 save_undo(_("change caption file for sub-album"),
2155                           proc {
2156                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2157                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2158                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2159                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2160                               xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2161                               my_gen_real_thumbnail.call
2162                               $notebook.set_page(0)
2163                               proc {
2164                                   perform_changefile.call
2165                                   $notebook.set_page(0)
2166                               }
2167                           })
2168             end
2169             fc.destroy
2170         }
2171
2172         refresh = proc {
2173             system("rm -f '#{thumbnail_file}'")
2174             my_gen_real_thumbnail.call
2175         }
2176
2177         rotate_and_cleanup = proc { |angle|
2178             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2179             system("rm -f '#{thumbnail_file}'")
2180         }
2181
2182         move = proc { |direction|
2183             $modified = true
2184
2185             save_changes('forced')
2186             oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2187             if direction == 'up'
2188                 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2189                 subalbums_edits_bypos[oldpos - 1][:position] += 1
2190             end
2191             if direction == 'down'
2192                 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2193                 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2194             end
2195             if direction == 'top'
2196                 for i in 1 .. oldpos - 1
2197                     subalbums_edits_bypos[i][:position] += 1
2198                 end
2199                 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2200             end
2201             if direction == 'bottom'
2202                 for i in oldpos + 1 .. subalbums_counter
2203                     subalbums_edits_bypos[i][:position] -= 1
2204                 end
2205                 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2206             end
2207
2208             elems = []
2209             $xmldir.elements.each('dir') { |element|
2210                 if (!element.attributes['deleted'])
2211                     elems << [ element.attributes['path'], element.remove ]
2212                 end
2213             }
2214             elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2215                   each { |e| $xmldir.add_element(e[1]) }
2216             #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2217             $xmldir.elements.each('descendant::dir') { |elem|
2218                 elem.delete_attribute('already-generated')
2219             }
2220
2221             sel = $albums_tv.selection.selected_rows
2222             change_dir
2223             populate_subalbums_treeview(false)
2224             $albums_tv.selection.select_path(sel[0])
2225         }
2226
2227         color_swap_and_cleanup = proc {
2228             perform_color_swap_and_cleanup = proc {
2229                 color_swap(xmldir, "#{infotype}-")
2230                 my_gen_real_thumbnail.call
2231             }
2232             perform_color_swap_and_cleanup.call
2233
2234             save_undo(_("color swap"),
2235                       proc {
2236                           perform_color_swap_and_cleanup.call
2237                           $notebook.set_page(0)
2238                           proc {
2239                               perform_color_swap_and_cleanup.call
2240                               $notebook.set_page(0)
2241                           }
2242                       })
2243         }
2244
2245         change_frame_offset_and_cleanup = proc {
2246             if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2247                 perform_change_frame_offset_and_cleanup = proc { |val|
2248                     change_frame_offset(xmldir, "#{infotype}-", val)
2249                     my_gen_real_thumbnail.call
2250                 }
2251                 perform_change_frame_offset_and_cleanup.call(values[:new])
2252
2253                 save_undo(_("specify frame offset"),
2254                           proc {
2255                               perform_change_frame_offset_and_cleanup.call(values[:old])
2256                               $notebook.set_page(0)
2257                               proc {
2258                                   perform_change_frame_offset_and_cleanup.call(values[:new])
2259                                   $notebook.set_page(0)
2260                               }
2261                           })
2262             end
2263         }
2264
2265         whitebalance_and_cleanup = proc {
2266             if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2267                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2268                 perform_change_whitebalance_and_cleanup = proc { |val|
2269                     change_whitebalance(xmldir, "#{infotype}-", val)
2270                     recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2271                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2272                     system("rm -f '#{thumbnail_file}'")
2273                 }
2274                 perform_change_whitebalance_and_cleanup.call(values[:new])
2275                 
2276                 save_undo(_("fix white balance"),
2277                           proc {
2278                               perform_change_whitebalance_and_cleanup.call(values[:old])
2279                               $notebook.set_page(0)
2280                               proc {
2281                                   perform_change_whitebalance_and_cleanup.call(values[:new])
2282                                   $notebook.set_page(0)
2283                               }
2284                           })
2285             end
2286         }
2287
2288         enhance_and_cleanup = proc {
2289             perform_enhance_and_cleanup = proc {
2290                 enhance(xmldir, "#{infotype}-")
2291                 my_gen_real_thumbnail.call
2292             }
2293             
2294             perform_enhance_and_cleanup.call
2295             
2296             save_undo(_("enhance"),
2297                       proc {
2298                           perform_enhance_and_cleanup.call
2299                           $notebook.set_page(0)
2300                           proc {
2301                               perform_enhance_and_cleanup.call
2302                               $notebook.set_page(0)
2303                           }
2304                       })
2305         }
2306
2307         evtbox.signal_connect('button-press-event') { |w, event|
2308             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2309                 if $r90.active?
2310                     rotate_and_cleanup.call(90)
2311                 elsif $r270.active?
2312                     rotate_and_cleanup.call(-90)
2313                 elsif $enhance.active?
2314                     enhance_and_cleanup.call
2315                 end
2316             end
2317             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2318                 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2319                                      { :forbid_left => true, :forbid_right => true,
2320                                        :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2321                                        :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2322                                      { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2323                                        :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2324                                        :refresh => refresh })
2325             end
2326             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2327                 change_image.call
2328                 true   #- handled
2329             end
2330         }
2331         evtbox.signal_connect('button-press-event') { |w, event|
2332             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2333             false
2334         }
2335
2336         evtbox.signal_connect('button-release-event') { |w, event|
2337             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2338                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2339                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2340                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2341                     msg 3, "gesture rotate: #{angle}"
2342                     rotate_and_cleanup.call(angle)
2343                 end
2344             end
2345             $gesture_press = nil
2346         }
2347                 
2348         $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2349         $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2350         current_y_sub_albums += 1
2351     }
2352
2353     if $xmldir.child_byname_notattr('dir', 'deleted')
2354         #- title edition
2355         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2356         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2357         $subalbums_title.set_justification(Gtk::Justification::CENTER)
2358         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2359         #- this album image/caption
2360         if $xmldir.attributes['thumbnails-caption']
2361             add_subalbum.call($xmldir, 0)
2362         end
2363     end
2364     total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2365     $xmldir.elements.each { |element|
2366         if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2367             #- element (image or video) of this album
2368             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2369             msg 3, "dest_img: #{dest_img}"
2370             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2371             total[element.name] += 1
2372         end
2373         if element.name == 'dir' && !element.attributes['deleted']
2374             #- sub-album image/caption
2375             add_subalbum.call(element, subalbums_counter += 1)
2376             total[element.name] += 1
2377         end
2378     }
2379     $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2380                                                                                 total['image'], total['video'], total['dir'] ]))
2381     $subalbums_vb.add($subalbums)
2382     $subalbums_vb.show_all
2383
2384     if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2385         $notebook.get_tab_label($autotable_sw).sensitive = false
2386         $notebook.set_page(0)
2387         $thumbnails_title.buffer.text = ''
2388     else
2389         $notebook.get_tab_label($autotable_sw).sensitive = true
2390         $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2391     end
2392
2393     if !$xmldir.child_byname_notattr('dir', 'deleted')
2394         $notebook.get_tab_label($subalbums_sw).sensitive = false
2395         $notebook.set_page(1)
2396     else
2397         $notebook.get_tab_label($subalbums_sw).sensitive = true
2398     end
2399 end
2400
2401 def pixbuf_or_nil(filename)
2402     begin
2403         return Gdk::Pixbuf.new(filename)
2404     rescue
2405         return nil
2406     end
2407 end
2408
2409 def theme_choose(current)
2410     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2411                              $main_window,
2412                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2413                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2414                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2415
2416     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2417     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2418     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2419     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2420     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2421     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2422     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2423     treeview.signal_connect('button-press-event') { |w, event|
2424         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2425             dialog.response(Gtk::Dialog::RESPONSE_OK)
2426         end
2427     }
2428
2429     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2430
2431     `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2432         dir.chomp!
2433         iter = model.append
2434         iter[0] = File.basename(dir)
2435         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2436         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2437         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2438         if File.basename(dir) == current
2439             treeview.selection.select_iter(iter)
2440         end
2441     }
2442
2443     dialog.set_default_size(700, 400)
2444     dialog.vbox.show_all
2445     dialog.run { |response|
2446         iter = treeview.selection.selected
2447         dialog.destroy
2448         if response == Gtk::Dialog::RESPONSE_OK && iter
2449             return model.get_value(iter, 0)
2450         end
2451     }
2452     return nil
2453 end
2454
2455 def show_password_protections
2456     examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2457         child_iter = $albums_iters[xmldir.attributes['path']]
2458         if xmldir.attributes['password-protect']
2459             child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2460             already_protected = true
2461         elsif already_protected
2462             pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2463             if pix
2464                 pix = pix.saturate_and_pixelate(1, true)
2465             end
2466             child_iter[2] = pix
2467         else
2468             child_iter[2] = nil
2469         end
2470         xmldir.elements.each('dir') { |elem|
2471             if !elem.attributes['deleted']
2472                 examine_dir_elem.call(child_iter, elem, already_protected)
2473             end
2474         }
2475     }
2476     examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2477 end
2478
2479 def populate_subalbums_treeview(select_first)
2480     $albums_ts.clear
2481     $autotable.clear
2482     $albums_iters = {}
2483     $subalbums_vb.children.each { |chld|
2484         $subalbums_vb.remove(chld)
2485     }
2486
2487     source = $xmldoc.root.attributes['source']
2488     msg 3, "source: #{source}"
2489
2490     xmldir = $xmldoc.elements['//dir']
2491     if !xmldir || xmldir.attributes['path'] != source
2492         msg 1, _("Corrupted booh file...")
2493         return
2494     end
2495
2496     append_dir_elem = proc { |parent_iter, xmldir|
2497         child_iter = $albums_ts.append(parent_iter)
2498         child_iter[0] = File.basename(xmldir.attributes['path'])
2499         child_iter[1] = xmldir.attributes['path']
2500         $albums_iters[xmldir.attributes['path']] = child_iter
2501         msg 3, "puttin location: #{xmldir.attributes['path']}"
2502         xmldir.elements.each('dir') { |elem|
2503             if !elem.attributes['deleted']
2504                 append_dir_elem.call(child_iter, elem)
2505             end
2506         }
2507     }
2508     append_dir_elem.call(nil, xmldir)
2509     show_password_protections
2510
2511     $albums_tv.expand_all
2512     if select_first
2513         $albums_tv.selection.select_iter($albums_ts.iter_first)
2514     end
2515 end
2516
2517 def open_file(filename)
2518
2519     $filename = nil
2520     $modified = false
2521     $current_path = nil   #- invalidate
2522     $modified_pixbufs = {}
2523     $albums_ts.clear
2524     $autotable.clear
2525     $subalbums_vb.children.each { |chld|
2526         $subalbums_vb.remove(chld)
2527     }
2528
2529     if !File.exists?(filename)
2530         return utf8(_("File not found."))
2531     end
2532
2533     begin
2534         $xmldoc = REXML::Document.new File.new(filename)
2535     rescue Exception
2536         $xmldoc = nil
2537     end
2538
2539     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2540         if entry2type(filename).nil?
2541             return utf8(_("Not a booh file!"))
2542         else
2543             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."))
2544         end
2545     end
2546
2547     if !source = $xmldoc.root.attributes['source']
2548         return utf8(_("Corrupted booh file..."))
2549     end
2550
2551     if !dest = $xmldoc.root.attributes['destination']
2552         return utf8(_("Corrupted booh file..."))
2553     end
2554
2555     if !theme = $xmldoc.root.attributes['theme']
2556         return utf8(_("Corrupted booh file..."))
2557     end
2558
2559     if $xmldoc.root.attributes['version'] < '0.8.4'
2560         msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2561         mark_document_as_dirty
2562         if $xmldoc.root.attributes['version'] < '0.8.4'
2563             msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2564             `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2565                 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2566                 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2567                 if old_dest_dir != new_dest_dir
2568                     sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2569                 end
2570                 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2571                     xmldir.elements.each { |element|
2572                         if %w(image video).include?(element.name) && !element.attributes['deleted']
2573                             old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2574                             new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2575                             Dir[old_name + '*'].each { |file|
2576                                 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2577                                 file != new_file and sys("mv '#{file}' '#{new_file}'")
2578                             }
2579                         end
2580                         if element.name == 'dir' && !element.attributes['deleted']
2581                             old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2582                             new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2583                             old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2584                         end
2585                     }
2586                 else
2587                     msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2588                 end
2589             }
2590         end
2591         $xmldoc.root.add_attribute('version', $VERSION)
2592     end
2593
2594     limit_sizes = $xmldoc.root.attributes['limit-sizes']
2595     optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2596     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2597
2598     $filename = filename
2599     select_theme(theme, limit_sizes, optimizefor32, nperrow)
2600     $default_size['thumbnails'] =~ /(.*)x(.*)/
2601     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2602     $albums_thumbnail_size =~ /(.*)x(.*)/
2603     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2604
2605     populate_subalbums_treeview(true)
2606
2607     $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
2608     return nil
2609 end
2610
2611 def open_file_user(filename)
2612     result = open_file(filename)
2613     if !result
2614         $config['last-opens'] ||= []
2615         if $config['last-opens'][-1] != utf8(filename)
2616             $config['last-opens'] << utf8(filename)
2617         end
2618         $orig_filename = $filename
2619         tmp = Tempfile.new("boohtemp")
2620         tmp.close!
2621         #- for security
2622         ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2623         ios.close
2624         $tempfiles << $filename << "#{$filename}.backup"
2625     else
2626         $orig_filename = nil
2627     end
2628     return result
2629 end
2630
2631 def open_file_popup
2632     if !ask_save_modifications(utf8(_("Save this album?")),
2633                                utf8(_("Do you want to save the changes to this album?")),
2634                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2635         return
2636     end
2637     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2638                                     nil,
2639                                     Gtk::FileChooser::ACTION_OPEN,
2640                                     nil,
2641                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2642     fc.add_shortcut_folder(File.expand_path("~/.booh"))
2643     fc.set_current_folder(File.expand_path("~/.booh"))
2644     fc.transient_for = $main_window
2645     ok = false
2646     while !ok
2647         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2648             push_mousecursor_wait(fc)
2649             msg = open_file_user(fc.filename)
2650             pop_mousecursor(fc)
2651             if msg
2652                 show_popup(fc, msg)
2653                 ok = false
2654             else
2655                 ok = true
2656             end
2657         else
2658             ok = true
2659         end
2660     end
2661     fc.destroy
2662 end
2663
2664 def open_url(url)
2665     cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2666     msg 2, cmd
2667     system(cmd)
2668 end
2669
2670 def additional_booh_options
2671     options = ''
2672     if $config['mproc']
2673         options += "--mproc #{$config['mproc'].to_i} "
2674     end
2675     options += "--comments-format '#{$config['comments-format']}'"
2676     return options
2677 end
2678
2679 def new_album
2680     if !ask_save_modifications(utf8(_("Save this album?")),
2681                                utf8(_("Do you want to save the changes to this album?")),
2682                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2683         return
2684     end
2685     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2686                              $main_window,
2687                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2688                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2689                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2690     
2691     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2692     tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2693                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2694     tbl.attach(src = Gtk::Entry.new,
2695                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2696     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2697                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2698     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2699                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2700     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2701                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2702     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2703                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2704     tbl.attach(dest = Gtk::Entry.new,
2705                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2706     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2707                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2708     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2709                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2710     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2711                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2712     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2713                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2714
2715     tooltips = Gtk::Tooltips.new
2716     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2717     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2718                          pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2719     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2720                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
2721     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2722     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)
2723     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2724                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2725     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2726                                    pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
2727     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)
2728
2729     src_nb_calculated_for = ''
2730     src_nb_thread = nil
2731     process_src_nb = proc {
2732         if src.text != src_nb_calculated_for
2733             src_nb_calculated_for = src.text
2734             if src_nb_thread
2735                 Thread.kill(src_nb_thread)
2736                 src_nb_thread = nil
2737             end
2738             if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
2739                 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
2740             else
2741                 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2742                     if File.readable?(from_utf8_safe(src_nb_calculated_for))
2743                         src_nb_thread = Thread.new {
2744                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2745                             total = { 'image' => 0, 'video' => 0, nil => 0 }
2746                             `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2747                                 if File.basename(dir) =~ /^\./
2748                                     next
2749                                 else
2750                                     begin
2751                                         Dir.entries(dir.chomp).each { |file|
2752                                             total[entry2type(file)] += 1
2753                                         }
2754                                     rescue Errno::EACCES, Errno::ENOENT
2755                                     end
2756                                 end
2757                             }
2758                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2759                             src_nb_thread = nil
2760                         }
2761                     else
2762                         src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2763                     end
2764                 else
2765                     src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2766                 end
2767             end
2768         end
2769         true
2770     }
2771     timeout_src_nb = Gtk.timeout_add(100) {
2772         process_src_nb.call
2773     }
2774
2775     src_browse.signal_connect('clicked') {
2776         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2777                                         nil,
2778                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
2779                                         nil,
2780                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2781         fc.transient_for = $main_window
2782         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2783             src.text = utf8(fc.filename)
2784             process_src_nb.call
2785             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2786         end
2787         fc.destroy
2788     }
2789
2790     dest_browse.signal_connect('clicked') {
2791         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2792                                         nil,
2793                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
2794                                         nil,
2795                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2796         fc.transient_for = $main_window
2797         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2798             dest.text = utf8(fc.filename)
2799         end
2800         fc.destroy
2801     }
2802
2803     conf_browse.signal_connect('clicked') {
2804         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2805                                         nil,
2806                                         Gtk::FileChooser::ACTION_SAVE,
2807                                         nil,
2808                                         [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2809         fc.transient_for = $main_window
2810         fc.add_shortcut_folder(File.expand_path("~/.booh"))
2811         fc.set_current_folder(File.expand_path("~/.booh"))
2812         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2813             conf.text = utf8(fc.filename)
2814         end
2815         fc.destroy
2816     }
2817
2818     theme_sizes = []
2819     nperrows = []
2820     recreate_theme_config = proc {
2821         theme_sizes.each { |e| sizes.remove(e[:widget]) }
2822         theme_sizes = []
2823         select_theme(theme_button.label, 'all', optimize432.active?, nil)
2824         $images_size.each { |s|
2825             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2826             if !s['optional']
2827                 cb.active = true
2828             end
2829             tooltips.set_tip(cb, utf8(s['description']), nil)
2830             theme_sizes << { :widget => cb, :value => s['name'] }
2831         }
2832         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2833         tooltips = Gtk::Tooltips.new
2834         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2835         theme_sizes << { :widget => cb, :value => 'original' }
2836         sizes.show_all
2837
2838         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2839         nperrow_group = nil
2840         nperrows = []
2841         $allowed_N_values.each { |n|
2842             if nperrow_group
2843                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2844             else
2845                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2846             end
2847             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
2848             if $default_N == n
2849                 rb.active = true
2850             end
2851             nperrows << { :widget => rb, :value => n }
2852         }
2853         nperrowradios.show_all
2854     }
2855     recreate_theme_config.call
2856
2857     theme_button.signal_connect('clicked') {
2858         if newtheme = theme_choose(theme_button.label)
2859             theme_button.label = newtheme
2860             recreate_theme_config.call
2861         end
2862     }
2863
2864     dialog.vbox.add(frame1)
2865     dialog.vbox.add(frame2)
2866     dialog.window_position = Gtk::Window::POS_MOUSE
2867     dialog.show_all
2868
2869     keepon = true
2870     ok = true
2871     while keepon
2872         dialog.run { |response|
2873             if response == Gtk::Dialog::RESPONSE_OK
2874                 srcdir = from_utf8_safe(src.text)
2875                 destdir = from_utf8_safe(dest.text)
2876                 confpath = from_utf8_safe(conf.text)
2877                 if src.text != '' && srcdir == ''
2878                     show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
2879                     src.grab_focus
2880                 elsif !File.directory?(srcdir)
2881                     show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2882                     src.grab_focus
2883                 elsif dest.text != '' && destdir == ''
2884                     show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
2885                     dest.grab_focus
2886                 elsif destdir != make_dest_filename(destdir)
2887                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2888                     dest.grab_focus
2889                 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
2890                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2891                     dest.grab_focus
2892                 elsif File.exists?(destdir) && !File.directory?(destdir)
2893                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2894                     dest.grab_focus
2895                 elsif conf.text == ''
2896                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2897                     conf.grab_focus
2898                 elsif conf.text != '' && confpath == ''
2899                     show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
2900                     conf.grab_focus
2901                 elsif File.directory?(confpath)
2902                     show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
2903                     conf.grab_focus
2904                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2905                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2906                 else
2907                     system("mkdir '#{destdir}'")
2908                     if !File.directory?(destdir)
2909                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2910                         dest.grab_focus
2911                     else
2912                         keepon = false
2913                     end
2914                 end
2915             else
2916                 keepon = ok = false
2917             end
2918         }
2919     end
2920     if ok
2921         srcdir = from_utf8(src.text)
2922         destdir = from_utf8(dest.text)
2923         configskel = File.expand_path(from_utf8(conf.text))
2924         theme = theme_button.label
2925         sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2926         nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2927         opt432 = optimize432.active?
2928         madewith = madewithentry.text
2929     end
2930     if src_nb_thread
2931         Thread.kill(src_nb_thread)
2932         gtk_thread_flush  #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2933     end
2934     dialog.destroy
2935     Gtk.timeout_remove(timeout_src_nb)
2936
2937     if ok
2938         call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2939                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2940                      "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' #{additional_booh_options}",
2941                      utf8(_("Please wait while scanning source directory...")),
2942                      'full scan',
2943                      { :closure_after => proc { open_file_user(configskel) } })
2944     end
2945 end
2946
2947 def properties
2948     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2949                              $main_window,
2950                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2951                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2952                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2953     
2954     source = $xmldoc.root.attributes['source']
2955     dest = $xmldoc.root.attributes['destination']
2956     theme = $xmldoc.root.attributes['theme']
2957     opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2958     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2959     limit_sizes = $xmldoc.root.attributes['limit-sizes']
2960     if limit_sizes
2961         limit_sizes = limit_sizes.split(/,/)
2962     end
2963     madewith = $xmldoc.root.attributes['made-with']
2964
2965     tooltips = Gtk::Tooltips.new
2966     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2967     tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2968                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2969     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2970                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2971     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2972                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2973     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
2974                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2975     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2976                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2977     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
2978                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2979
2980     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2981     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2982                                    pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2983     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2984                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
2985     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2986     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)
2987     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2988                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2989     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2990                                    pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
2991     if madewith
2992         madewithentry.text = madewith
2993     end
2994     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)
2995
2996     theme_sizes = []
2997     nperrows = []
2998     recreate_theme_config = proc {
2999         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3000         theme_sizes = []
3001         select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3002
3003         $images_size.each { |s|
3004             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3005             if limit_sizes
3006                 if limit_sizes.include?(s['name'])
3007                     cb.active = true
3008                 end
3009             else
3010                 if !s['optional']
3011                     cb.active = true
3012                 end
3013             end
3014             tooltips.set_tip(cb, utf8(s['description']), nil)
3015             theme_sizes << { :widget => cb, :value => s['name'] }
3016         }
3017         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3018         tooltips = Gtk::Tooltips.new
3019         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3020         if limit_sizes && limit_sizes.include?('original')
3021             cb.active = true
3022         end
3023         theme_sizes << { :widget => cb, :value => 'original' }
3024         sizes.show_all
3025
3026         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3027         nperrow_group = nil
3028         nperrows = []
3029         $allowed_N_values.each { |n|
3030             if nperrow_group
3031                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3032             else
3033                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3034             end
3035             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3036             nperrowradios.add(Gtk::Label.new('  '))
3037             if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3038                 rb.active = true
3039             end
3040             nperrows << { :widget => rb, :value => n.to_s }
3041         }
3042         nperrowradios.show_all
3043     }
3044     recreate_theme_config.call
3045
3046     theme_button.signal_connect('clicked') {
3047         if newtheme = theme_choose(theme_button.label)
3048             limit_sizes = nil
3049             nperrow = nil
3050             theme_button.label = newtheme
3051             recreate_theme_config.call
3052         end
3053     }
3054
3055     dialog.vbox.add(frame1)
3056     dialog.vbox.add(frame2)
3057     dialog.window_position = Gtk::Window::POS_MOUSE
3058     dialog.show_all
3059
3060     keepon = true
3061     ok = true
3062     while keepon
3063         dialog.run { |response|
3064             if response == Gtk::Dialog::RESPONSE_OK
3065                 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3066                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3067                 else
3068                     keepon = false
3069                 end
3070             else
3071                 keepon = ok = false
3072             end
3073         }
3074     end
3075     save_theme = theme_button.label
3076     save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3077     save_opt432 = optimize432.active?
3078     save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3079     save_madewith = madewithentry.text
3080     dialog.destroy
3081
3082     if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith)
3083         mark_document_as_dirty
3084         save_current_file
3085         call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3086                      "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3087                      "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' #{additional_booh_options}",
3088                      utf8(_("Please wait while scanning source directory...")),
3089                      'full scan',
3090                      { :closure_after => proc {
3091                              open_file($filename)
3092                              $modified = true
3093                          } })
3094     end
3095 end
3096
3097 def merge_current
3098     save_current_file
3099
3100     sel = $albums_tv.selection.selected_rows
3101
3102     call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3103                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
3104                  utf8(_("Please wait while scanning source directory...")),
3105                  'one dir scan',
3106                  { :closure_after => proc {
3107                          open_file($filename)
3108                          $albums_tv.selection.select_path(sel[0])
3109                          $modified = true
3110                      } })
3111 end
3112
3113 def merge_newsubs
3114     save_current_file
3115
3116     sel = $albums_tv.selection.selected_rows
3117
3118     call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3119                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
3120                  utf8(_("Please wait while scanning source directory...")),
3121                  'subdirs scan',
3122                  { :closure_after => proc {
3123                          open_file($filename)
3124                          $albums_tv.selection.select_path(sel[0])
3125                          $modified = true
3126                      } })
3127 end
3128
3129 def merge
3130     save_current_file
3131
3132     theme = $xmldoc.root.attributes['theme']
3133     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3134     if limit_sizes
3135         limit_sizes = "--sizes #{limit_sizes}"
3136     end
3137     call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3138                  "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3139                  utf8(_("Please wait while scanning source directory...")),
3140                  'full scan',
3141                  { :closure_after => proc {
3142                          open_file($filename)
3143                          $modified = true
3144                      } })
3145 end
3146
3147 def save_as_do
3148     fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3149                                     nil,
3150                                     Gtk::FileChooser::ACTION_SAVE,
3151                                     nil,
3152                                     [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3153     fc.transient_for = $main_window
3154     fc.add_shortcut_folder(File.expand_path("~/.booh"))
3155     fc.set_current_folder(File.expand_path("~/.booh"))
3156     fc.filename = $orig_filename
3157     if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3158         $orig_filename = fc.filename
3159         if ! save_current_file_user
3160             fc.destroy
3161             return save_as_do
3162         end
3163         $config['last-opens'] ||= []
3164         $config['last-opens'] << $orig_filename
3165     end
3166     fc.destroy
3167 end
3168
3169 def preferences
3170     dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3171                              $main_window,
3172                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3173                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3174                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3175
3176     dialog.vbox.add(notebook = Gtk::Notebook.new)
3177     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3178     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3179                0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3180     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)),
3181                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3182     tooltips = Gtk::Tooltips.new
3183     tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3184 for example: /usr/bin/mplayer %f")), nil)
3185     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3186                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3187     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3188                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3189     tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3190 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3191     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3192                0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3193     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)),
3194                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3195     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)
3196     tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3197                0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3198     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)
3199     tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3200                0, 2, 5, 6, Gtk::FILL, Gtk::SHRINK, 2, 2)
3201     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)
3202
3203     smp_check.signal_connect('toggled') {
3204         if smp_check.active?
3205             smp_hbox.sensitive = true
3206         else
3207             smp_hbox.sensitive = false
3208         end
3209     }
3210     if $config['mproc']
3211         smp_check.active = true
3212         smp_spin.value = $config['mproc'].to_i
3213     end
3214     nogestures_check.active = $config['nogestures']
3215     deleteondisk_check.active = $config['deleteondisk']
3216
3217     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3218     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3219                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3220     tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3221                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3222     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3223                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3224     tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3225                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3226     tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3227                2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3228     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)
3229     commentsformat_help.signal_connect('clicked') {
3230         show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3231 hence you should look at ImageMagick/identify documentation for the most    
3232 accurate and up-to-date documentation. Last time I checked, documentation
3233 was:
3234
3235 Print information about the image in a format of your choosing. You can
3236 include the image filename, type, width, height, Exif data, or other image
3237 attributes by embedding special format characters:                          
3238
3239                      %O   page offset
3240                      %P   page width and height                             
3241                      %b   file size                                         
3242                      %c   comment                                           
3243                      %d   directory                                         
3244                      %e   filename extension                                
3245                      %f   filename                                          
3246                      %g   page geometry                                     
3247                      %h   height                                            
3248                      %i   input filename                                    
3249                      %k   number of unique colors                           
3250                      %l   label                                             
3251                      %m   magick                                            
3252                      %n   number of scenes                                  
3253                      %o   output filename                                   
3254                      %p   page number                                       
3255                      %q   quantum depth                                     
3256                      %r   image class and colorspace                        
3257                      %s   scene number                                      
3258                      %t   top of filename                                   
3259                      %u   unique temporary filename                         
3260                      %w   width                                             
3261                      %x   x resolution                                      
3262                      %y   y resolution                                      
3263                      %z   image depth                                       
3264                      %@   bounding box                                      
3265                      %#   signature                                         
3266                      %%   a percent sign                                    
3267                                                                             
3268 For example,                                                                
3269                                                                             
3270     %m:%f %wx%h
3271                                                                             
3272 displays MIFF:bird.miff 512x480 for an image titled bird.miff and whose
3273 width is 512 and height is 480.                
3274                                                                             
3275 If the first character of string is @, the format is read from a file titled
3276 by the remaining characters in the string.
3277                                                                             
3278 You can also use the following special formatting syntax to print Exif
3279 information contained in the file:
3280                                                                             
3281     %[EXIF:tag]                                                             
3282                                                                             
3283 Where tag can be one of the following:                                      
3284                                                                             
3285     *  (print all Exif tags, in keyword=data format)                        
3286     !  (print all Exif tags, in tag_number data format)                     
3287     #hhhh (print data for Exif tag #hhhh)                                   
3288     ImageWidth                                                              
3289     ImageLength                                                             
3290     BitsPerSample                                                           
3291     Compression                                                             
3292     PhotometricInterpretation                                               
3293     FillOrder                                                               
3294     DocumentName                                                            
3295     ImageDescription                                                        
3296     Make                                                                    
3297     Model                                                                   
3298     StripOffsets                                                            
3299     Orientation                                                             
3300     SamplesPerPixel                                                         
3301     RowsPerStrip                                                            
3302     StripByteCounts                                                         
3303     XResolution                                                             
3304     YResolution                                                             
3305     PlanarConfiguration                                                     
3306     ResolutionUnit                                                          
3307     TransferFunction                                                        
3308     Software                                                                
3309     DateTime                                                                
3310     Artist                                                                  
3311     WhitePoint                                                              
3312     PrimaryChromaticities                                                   
3313     TransferRange                                                           
3314     JPEGProc                                                                
3315     JPEGInterchangeFormat                                                   
3316     JPEGInterchangeFormatLength                                             
3317     YCbCrCoefficients                                                       
3318     YCbCrSubSampling                                                        
3319     YCbCrPositioning                                                        
3320     ReferenceBlackWhite                                                     
3321     CFARepeatPatternDim                                                     
3322     CFAPattern                                                              
3323     BatteryLevel                                                            
3324     Copyright                                                               
3325     ExposureTime                                                            
3326     FNumber                                                                 
3327     IPTC/NAA                                                                
3328     ExifOffset                                                              
3329     InterColorProfile                                                       
3330     ExposureProgram                                                         
3331     SpectralSensitivity                                                     
3332     GPSInfo                                                                 
3333     ISOSpeedRatings                                                         
3334     OECF                                                                    
3335     ExifVersion                                                             
3336     DateTimeOriginal                                                        
3337     DateTimeDigitized                                                       
3338     ComponentsConfiguration                                                 
3339     CompressedBitsPerPixel                                                  
3340     ShutterSpeedValue                                                       
3341     ApertureValue                                                           
3342     BrightnessValue                                                         
3343     ExposureBiasValue                                                       
3344     MaxApertureValue                                                        
3345     SubjectDistance                                                         
3346     MeteringMode                                                            
3347     LightSource                                                             
3348     Flash                                                                   
3349     FocalLength                                                             
3350     MakerNote                                                               
3351     UserComment                                                             
3352     SubSecTime                                                              
3353     SubSecTimeOriginal                                                      
3354     SubSecTimeDigitized                                                     
3355     FlashPixVersion                                                         
3356     ColorSpace                                                              
3357     ExifImageWidth                                                          
3358     ExifImageLength                                                         
3359     InteroperabilityOffset                                                  
3360     FlashEnergy                                                             
3361     SpatialFrequencyResponse                                                
3362     FocalPlaneXResolution                                                   
3363     FocalPlaneYResolution                                                   
3364     FocalPlaneResolutionUnit                                                
3365     SubjectLocation                                                         
3366     ExposureIndex                                                           
3367     SensingMethod                                                           
3368     FileSource                                                              
3369     SceneType")), { :scrolled => true })
3370     }
3371
3372     dialog.vbox.show_all
3373     dialog.run { |response|
3374         if response == Gtk::Dialog::RESPONSE_OK
3375             $config['video-viewer'] = video_viewer_entry.text
3376             $config['browser'] = browser_entry.text
3377             if smp_check.active?
3378                 $config['mproc'] = smp_spin.value.to_i
3379             else
3380                 $config.delete('mproc')
3381             end
3382             $config['nogestures'] = nogestures_check.active?
3383             $config['deleteondisk'] = deleteondisk_check.active?
3384
3385             $config['convert-enhance'] = enhance_entry.text
3386             $config['comments-format'] = commentsformat_entry.text.gsub(/'/, '')
3387         end
3388     }
3389     dialog.destroy
3390 end
3391