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