defend against ruby-1.8.4
[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 &nbs