exif is faster and more reliable
[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/libadds'
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(false)
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(false)
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(false)
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).to_s)
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.to_s)
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(`exif -m #{fullpath}`),
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(0)
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') {
1898             open_url(options[0][:linkurl] + '/index.html')
1899             dialog.response(Gtk::Dialog::RESPONSE_OK)
1900             set_mousecursor_normal
1901         }
1902         linkbut.relief = Gtk::RELIEF_NONE
1903         linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1904         linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1905         dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1906     end
1907
1908     dialog.show_all
1909
1910     if !options[0] || !options[0][:not_transient]
1911         dialog.transient_for = parent
1912         dialog.run { |response|
1913             dialog.destroy
1914             if options[0] && options[0][:okcancel]
1915                 return response == Gtk::Dialog::RESPONSE_OK
1916             end
1917         }
1918     else
1919         dialog.signal_connect('response') { dialog.destroy }
1920     end
1921 end
1922
1923 def backend_wait_message(parent, msg, infopipe_path, mode)
1924     w = Gtk::Window.new
1925     w.set_transient_for(parent)
1926     w.modal = true
1927
1928     vb = Gtk::VBox.new(false, 5).set_border_width(5)
1929     vb.pack_start(Gtk::Label.new(msg), false, false)
1930
1931     vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1932     vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1933     if mode != 'one dir scan'
1934         vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1935     end
1936     if mode == 'web-album'
1937         vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1938         vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1939     end
1940     vb.pack_start(Gtk::HSeparator.new, false, false)
1941
1942     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1943     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1944     vb.pack_end(bottom, false, false)
1945
1946     infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1947     refresh_thread = Thread.new {
1948         directories_counter = 0
1949         while line = infopipe.gets
1950             if line =~ /^directories: (\d+), sizes: (\d+)/
1951                 directories = $1.to_f + 1
1952                 sizes = $2.to_f
1953             elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1954                 elements = $3.to_f + 1
1955                 if mode == 'web-album'
1956                     elements += sizes
1957                 end
1958                 element_counter = 0
1959                 gtk_thread_protect { pb1_1.fraction = 0 }
1960                 if mode != 'one dir scan'
1961                     newtext = utf8(full_src_dir_to_rel($1, $2))
1962                     newtext = '/' if newtext == ''
1963                     gtk_thread_protect { pb1_2.text = newtext }
1964                     directories_counter += 1
1965                     gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1966                 end
1967             elsif line =~ /^processing element$/
1968                 element_counter += 1
1969                 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1970             elsif line =~ /^processing size$/
1971                 element_counter += 1
1972                 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1973             elsif line =~ /^finished processing sizes$/
1974                 gtk_thread_protect { pb1_1.fraction = 1 }
1975             elsif line =~ /^creating index.html$/
1976                 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1977                 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1978                 directories_counter = 0
1979             elsif line =~ /^index.html: (.+)\|(.+)/
1980                 newtext = utf8(full_src_dir_to_rel($1, $2))
1981                 newtext = '/' if newtext == ''
1982                 gtk_thread_protect { pb2.text = newtext }
1983                 directories_counter += 1
1984                 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1985             elsif line =~ /^die: (.*)$/
1986                 $diemsg = $1
1987             end
1988         end
1989     }
1990
1991     w.add(vb)
1992     w.signal_connect('delete-event') { w.destroy }
1993     w.signal_connect('destroy') {
1994         Thread.kill(refresh_thread)
1995         gtk_thread_flush  #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1996         if infopipe_path
1997             infopipe.close
1998             system("rm -f #{infopipe_path}")
1999         end
2000     }
2001     w.window_position = Gtk::Window::POS_CENTER
2002     w.show_all
2003
2004     return [ b, w ]
2005 end
2006
2007 def call_backend(cmd, waitmsg, mode, params)
2008     pipe = Tempfile.new("boohpipe")
2009     pipe.close!
2010     system("mkfifo #{pipe.path}")
2011     cmd += " --info-pipe #{pipe.path}"
2012     button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2013     pid = nil
2014     Thread.new {
2015         msg 2, cmd
2016         if pid = fork
2017             id, exitstatus = Process.waitpid2(pid)
2018             gtk_thread_protect { w8.destroy }
2019             if exitstatus == 0
2020                 if params[:successmsg]
2021                     gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2022                 end
2023                 if params[:closure_after]
2024                     gtk_thread_protect(&params[:closure_after])
2025                 end
2026             elsif exitstatus == 15
2027                 #- say nothing, user aborted
2028             else
2029                 gtk_thread_protect { show_popup($main_window,
2030                                                 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2031             end
2032         else
2033             exec(cmd)
2034         end
2035     }
2036     button.signal_connect('clicked') {
2037         Process.kill('SIGTERM', pid)
2038     }
2039 end
2040
2041 def save_changes(*forced)
2042     if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2043         return
2044     end
2045
2046     $xmldir.delete_attribute('already-generated')
2047
2048     propagate_children = proc { |xmldir|
2049         if xmldir.attributes['subdirs-caption']
2050             xmldir.delete_attribute('already-generated')
2051         end
2052         xmldir.elements.each('dir') { |element|
2053             propagate_children.call(element)
2054         }
2055     }
2056
2057     if $xmldir.child_byname_notattr('dir', 'deleted')
2058         new_title = $subalbums_title.buffer.text
2059         if new_title != $xmldir.attributes['subdirs-caption']
2060             parent = $xmldir.parent
2061             if parent.name == 'dir'
2062                 parent.delete_attribute('already-generated')
2063             end
2064             propagate_children.call($xmldir)
2065         end
2066         $xmldir.add_attribute('subdirs-caption', new_title)
2067         $xmldir.elements.each('dir') { |element|
2068             if !element.attributes['deleted']
2069                 path = element.attributes['path']
2070                 newtext = $subalbums_edits[path][:editzone].buffer.text
2071                 if element.attributes['subdirs-caption']
2072                     if element.attributes['subdirs-caption'] != newtext
2073                         propagate_children.call(element)
2074                     end
2075                     element.add_attribute('subdirs-caption',     newtext)
2076                     element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2077                 else
2078                     if element.attributes['thumbnails-caption'] != newtext
2079                         element.delete_attribute('already-generated')
2080                     end
2081                     element.add_attribute('thumbnails-caption',     newtext)
2082                     element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2083                 end
2084             end
2085         }
2086     end
2087
2088     if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2089         if $xmldir.attributes['thumbnails-caption']
2090             path = $xmldir.attributes['path']
2091             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2092         end
2093     elsif $xmldir.attributes['thumbnails-caption']
2094         $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2095     end
2096
2097     if $xmldir.attributes['thumbnails-caption']
2098         if edit = $subalbums_edits[$xmldir.attributes['path']]
2099             $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2100         end
2101     end
2102
2103     #- remove and reinsert elements to reflect new ordering
2104     saves = {}
2105     cpt = 0
2106     $xmldir.elements.each { |element|
2107         if element.name == 'image' || element.name == 'video'
2108             saves[element.attributes['filename']] = element.remove
2109             cpt += 1
2110         end
2111     }
2112     $autotable.current_order.each { |path|
2113         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2114         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2115         saves.delete(path)
2116     }
2117     saves.each_key { |path|
2118         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2119         chld.add_attribute('deleted', 'true')
2120     }
2121 end
2122
2123 def sort_by_exif_date
2124     $modified = true
2125     save_changes
2126     current_order = []
2127     $xmldir.elements.each { |element|
2128         if element.name == 'image' || element.name == 'video'
2129             current_order << element.attributes['filename']
2130         end
2131     }
2132
2133     #- look for EXIF dates
2134     w = Gtk::Window.new
2135     w.set_transient_for($main_window)
2136     w.modal = true
2137     vb = Gtk::VBox.new(false, 5).set_border_width(5)
2138     vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2139     vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2140     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2141     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2142     vb.pack_end(bottom, false, false)
2143     w.add(vb)
2144     w.signal_connect('delete-event') { w.destroy }
2145     w.window_position = Gtk::Window::POS_CENTER
2146     w.show_all
2147
2148     aborted = false
2149     b.signal_connect('clicked') { aborted = true }
2150     dates = {}
2151     i = 0
2152     current_order.each { |f|
2153         i += 1
2154         if entry2type(f) == 'image'
2155             pb.text = f
2156             pb.fraction = i.to_f / current_order.size
2157             Gtk.main_iteration while Gtk.events_pending?
2158             date_time = `identify -format "%[EXIF:DateTimeOriginal]" '#{from_utf8($current_path + "/" + f)}'`.chomp
2159             if $? == 0 && date_time != ''
2160                 dates[f] = date_time
2161             end
2162         end
2163         if aborted
2164             break
2165         end
2166     }
2167     w.destroy
2168     if aborted
2169         return
2170     end
2171
2172     saves = {}
2173     $xmldir.elements.each { |element|
2174         if element.name == 'image' || element.name == 'video'
2175             saves[element.attributes['filename']] = element.remove
2176         end
2177     }
2178
2179     #- find a good fallback for all entries without a date (still next to the item they were next to)
2180     neworder = dates.keys.sort { |a,b| dates[a] <=> dates[b] }
2181     for i in 0 .. current_order.size - 1
2182         if ! neworder.include?(current_order[i])
2183             j = i - 1
2184             while j > 0 && ! neworder.include?(current_order[j])
2185                 j -= 1
2186             end
2187             neworder[(neworder.index(current_order[j]) || -1 ) + 1, 0] = current_order[i]
2188         end
2189     end
2190     neworder.each { |f|
2191         $xmldir.add_element(saves[f].name, saves[f].attributes)
2192     }
2193
2194     #- let the auto-table reflect new ordering
2195     change_dir
2196 end
2197
2198 def remove_all_captions
2199     $modified = true
2200     texts = {}
2201     $autotable.current_order.each { |path|
2202         texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2203         $name2widgets[File.basename(path)][:textview].buffer.text = ''
2204     }
2205     save_undo(_("remove all captions"),
2206               proc { |texts|
2207                   texts.each_key { |key|
2208                       $name2widgets[key][:textview].buffer.text = texts[key]
2209                   }
2210                   $notebook.set_page(1)
2211                   proc {
2212                       texts.each_key { |key|
2213                           $name2widgets[key][:textview].buffer.text = ''
2214                       }
2215                       $notebook.set_page(1)
2216                   }
2217               }, texts)
2218 end
2219
2220 def change_dir
2221     $selected_elements.each_key { |path|
2222         $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2223     }
2224     $autotable.clear
2225     $vbox2widgets = {}
2226     $name2widgets = {}
2227     $name2closures = {}
2228     $selected_elements = {}
2229     $cuts = []
2230     $multiple_dnd = []
2231     UndoHandler.cleanup
2232     $undo_tb.sensitive = $undo_mb.sensitive = false
2233     $redo_tb.sensitive = $redo_mb.sensitive = false
2234
2235     if !$current_path
2236         return
2237     end
2238
2239     $subalbums_vb.children.each { |chld|
2240         $subalbums_vb.remove(chld)
2241     }
2242     $subalbums = Gtk::Table.new(0, 0, true)
2243     current_y_sub_albums = 0
2244
2245     $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2246     $subalbums_edits = {}
2247     subalbums_counter = 0
2248     subalbums_edits_bypos = {}
2249
2250     add_subalbum = proc { |xmldir, counter|
2251         $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2252         subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2253         if xmldir == $xmldir
2254             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2255             captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2256             caption = xmldir.attributes['thumbnails-caption']
2257             infotype = 'thumbnails'
2258         else
2259             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2260             captionfile, caption = find_subalbum_caption_info(xmldir)
2261             infotype = find_subalbum_info_type(xmldir)
2262         end
2263         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2264         hbox = Gtk::HBox.new
2265         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2266         f = Gtk::Frame.new
2267         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2268
2269         img = nil
2270         my_gen_real_thumbnail = proc {
2271             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2272         }
2273
2274         if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2275             f.add(img = Gtk::Image.new)
2276             my_gen_real_thumbnail.call
2277         else
2278             f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2279         end
2280         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2281         $subalbums.attach(hbox,
2282                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2283
2284         frame, textview = create_editzone($subalbums_sw, 0, img)
2285         textview.buffer.text = caption
2286         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2287                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2288
2289         change_image = proc {
2290             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2291                                             nil,
2292                                             Gtk::FileChooser::ACTION_OPEN,
2293                                             nil,
2294                                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2295             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2296             fc.transient_for = $main_window
2297             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))
2298             f.add(preview_img = Gtk::Image.new)
2299             preview.show_all
2300             fc.signal_connect('update-preview') { |w|
2301                 begin
2302                     if fc.preview_filename
2303                         preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2304                         fc.preview_widget_active = true
2305                     end
2306                 rescue Gdk::PixbufError
2307                     fc.preview_widget_active = false
2308                 end
2309             }
2310             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2311                 $modified = true
2312                 old_file = captionfile
2313                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2314                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2315                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2316                 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2317
2318                 new_file = fc.filename
2319                 msg 3, "new captionfile is: #{fc.filename}"
2320                 perform_changefile = proc {
2321                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2322                     $modified_pixbufs.delete(thumbnail_file)
2323                     xmldir.delete_attribute("#{infotype}-rotate")
2324                     xmldir.delete_attribute("#{infotype}-color-swap")
2325                     xmldir.delete_attribute("#{infotype}-enhance")
2326                     xmldir.delete_attribute("#{infotype}-frame-offset")
2327                     my_gen_real_thumbnail.call
2328                 }
2329                 perform_changefile.call
2330
2331                 save_undo(_("change caption file for sub-album"),
2332                           proc {
2333                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2334                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2335                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2336                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2337                               xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2338                               my_gen_real_thumbnail.call
2339                               $notebook.set_page(0)
2340                               proc {
2341                                   perform_changefile.call
2342                                   $notebook.set_page(0)
2343                               }
2344                           })
2345             end
2346             fc.destroy
2347         }
2348
2349         refresh = proc {
2350             system("rm -f '#{thumbnail_file}'")
2351             my_gen_real_thumbnail.call
2352         }
2353
2354         rotate_and_cleanup = proc { |angle|
2355             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2356             system("rm -f '#{thumbnail_file}'")
2357         }
2358
2359         move = proc { |direction|
2360             $modified = true
2361
2362             save_changes('forced')
2363             oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2364             if direction == 'up'
2365                 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2366                 subalbums_edits_bypos[oldpos - 1][:position] += 1
2367             end
2368             if direction == 'down'
2369                 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2370                 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2371             end
2372             if direction == 'top'
2373                 for i in 1 .. oldpos - 1
2374                     subalbums_edits_bypos[i][:position] += 1
2375                 end
2376                 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2377             end
2378             if direction == 'bottom'
2379                 for i in oldpos + 1 .. subalbums_counter
2380                     subalbums_edits_bypos[i][:position] -= 1
2381                 end
2382                 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2383             end
2384
2385             elems = []
2386             $xmldir.elements.each('dir') { |element|
2387                 if (!element.attributes['deleted'])
2388                     elems << [ element.attributes['path'], element.remove ]
2389                 end
2390             }
2391             elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2392                   each { |e| $xmldir.add_element(e[1]) }
2393             #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2394             $xmldir.elements.each('descendant::dir') { |elem|
2395                 elem.delete_attribute('already-generated')
2396             }
2397
2398             sel = $albums_tv.selection.selected_rows
2399             change_dir
2400             populate_subalbums_treeview(false)
2401             $albums_tv.selection.select_path(sel[0])
2402         }
2403
2404         color_swap_and_cleanup = proc {
2405             perform_color_swap_and_cleanup = proc {
2406                 color_swap(xmldir, "#{infotype}-")
2407                 my_gen_real_thumbnail.call
2408             }
2409             perform_color_swap_and_cleanup.call
2410
2411             save_undo(_("color swap"),
2412                       proc {
2413                           perform_color_swap_and_cleanup.call
2414                           $notebook.set_page(0)
2415                           proc {
2416                               perform_color_swap_and_cleanup.call
2417                               $notebook.set_page(0)
2418                           }
2419                       })
2420         }
2421
2422         change_frame_offset_and_cleanup = proc {
2423             if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2424                 perform_change_frame_offset_and_cleanup = proc { |val|
2425                     change_frame_offset(xmldir, "#{infotype}-", val)
2426                     my_gen_real_thumbnail.call
2427                 }
2428                 perform_change_frame_offset_and_cleanup.call(values[:new])
2429
2430                 save_undo(_("specify frame offset"),
2431                           proc {
2432                               perform_change_frame_offset_and_cleanup.call(values[:old])
2433                               $notebook.set_page(0)
2434                               proc {
2435                                   perform_change_frame_offset_and_cleanup.call(values[:new])
2436                                   $notebook.set_page(0)
2437                               }
2438                           })
2439             end
2440         }
2441
2442         whitebalance_and_cleanup = proc {
2443             if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2444                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2445                 perform_change_whitebalance_and_cleanup = proc { |val|
2446                     change_whitebalance(xmldir, "#{infotype}-", val)
2447                     recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2448                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2449                     system("rm -f '#{thumbnail_file}'")
2450                 }
2451                 perform_change_whitebalance_and_cleanup.call(values[:new])
2452                 
2453                 save_undo(_("fix white balance"),
2454                           proc {
2455                               perform_change_whitebalance_and_cleanup.call(values[:old])
2456                               $notebook.set_page(0)
2457                               proc {
2458                                   perform_change_whitebalance_and_cleanup.call(values[:new])
2459                                   $notebook.set_page(0)
2460                               }
2461                           })
2462             end
2463         }
2464
2465         gammacorrect_and_cleanup = proc {
2466             if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2467                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2468                 perform_change_gammacorrect_and_cleanup = proc { |val|
2469                     change_gammacorrect(xmldir, "#{infotype}-", val)
2470                     recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2471                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2472                     system("rm -f '#{thumbnail_file}'")
2473                 }
2474                 perform_change_gammacorrect_and_cleanup.call(values[:new])
2475                 
2476                 save_undo(_("gamma correction"),
2477                           proc {
2478                               perform_change_gammacorrect_and_cleanup.call(values[:old])
2479                               $notebook.set_page(0)
2480                               proc {
2481                                   perform_change_gammacorrect_and_cleanup.call(values[:new])
2482                                   $notebook.set_page(0)
2483                               }
2484                           })
2485             end
2486         }
2487
2488         enhance_and_cleanup = proc {
2489             perform_enhance_and_cleanup = proc {
2490                 enhance(xmldir, "#{infotype}-")
2491                 my_gen_real_thumbnail.call
2492             }
2493             
2494             perform_enhance_and_cleanup.call
2495             
2496             save_undo(_("enhance"),
2497                       proc {
2498                           perform_enhance_and_cleanup.call
2499                           $notebook.set_page(0)
2500                           proc {
2501                               perform_enhance_and_cleanup.call
2502                               $notebook.set_page(0)
2503                           }
2504                       })
2505         }
2506
2507         evtbox.signal_connect('button-press-event') { |w, event|
2508             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2509                 if $r90.active?
2510                     rotate_and_cleanup.call(90)
2511                 elsif $r270.active?
2512                     rotate_and_cleanup.call(-90)
2513                 elsif $enhance.active?
2514                     enhance_and_cleanup.call
2515                 end
2516             end
2517             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2518                 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2519                                      { :forbid_left => true, :forbid_right => true,
2520                                        :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2521                                        :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2522                                      { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2523                                        :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2524                                        :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2525             end
2526             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2527                 change_image.call
2528                 true   #- handled
2529             end
2530         }
2531         evtbox.signal_connect('button-press-event') { |w, event|
2532             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2533             false
2534         }
2535
2536         evtbox.signal_connect('button-release-event') { |w, event|
2537             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2538                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2539                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2540                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2541                     msg 3, "gesture rotate: #{angle}"
2542                     rotate_and_cleanup.call(angle)
2543                 end
2544             end
2545             $gesture_press = nil
2546         }
2547                 
2548         $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2549         $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2550         current_y_sub_albums += 1
2551     }
2552
2553     if $xmldir.child_byname_notattr('dir', 'deleted')
2554         #- title edition
2555         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2556         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2557         $subalbums_title.set_justification(Gtk::Justification::CENTER)
2558         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2559         #- this album image/caption
2560         if $xmldir.attributes['thumbnails-caption']
2561             add_subalbum.call($xmldir, 0)
2562         end
2563     end
2564     total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2565     $xmldir.elements.each { |element|
2566         if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2567             #- element (image or video) of this album
2568             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2569             msg 3, "dest_img: #{dest_img}"
2570             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2571             total[element.name] += 1
2572         end
2573         if element.name == 'dir' && !element.attributes['deleted']
2574             #- sub-album image/caption
2575             add_subalbum.call(element, subalbums_counter += 1)
2576             total[element.name] += 1
2577         end
2578     }
2579     $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2580                                                                                 total['image'], total['video'], total['dir'] ]))
2581     $subalbums_vb.add($subalbums)
2582     $subalbums_vb.show_all
2583
2584     if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2585         $notebook.get_tab_label($autotable_sw).sensitive = false
2586         $notebook.set_page(0)
2587         $thumbnails_title.buffer.text = ''
2588     else
2589         $notebook.get_tab_label($autotable_sw).sensitive = true
2590         $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2591     end
2592
2593     if !$xmldir.child_byname_notattr('dir', 'deleted')
2594         $notebook.get_tab_label($subalbums_sw).sensitive = false
2595         $notebook.set_page(1)
2596     else
2597         $notebook.get_tab_label($subalbums_sw).sensitive = true
2598     end
2599 end
2600
2601 def pixbuf_or_nil(filename)
2602     begin
2603         return Gdk::Pixbuf.new(filename)
2604     rescue
2605         return nil
2606     end
2607 end
2608
2609 def theme_choose(current)
2610     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2611                              $main_window,
2612                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2613                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2614                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2615
2616     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2617     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2618     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2619     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2620     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2621     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2622     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2623     treeview.signal_connect('button-press-event') { |w, event|
2624         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2625             dialog.response(Gtk::Dialog::RESPONSE_OK)
2626         end
2627     }
2628
2629     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2630
2631     `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2632         dir.chomp!
2633         iter = model.append
2634         iter[0] = File.basename(dir)
2635         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2636         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2637         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2638         if File.basename(dir) == current
2639             treeview.selection.select_iter(iter)
2640         end
2641     }
2642
2643     dialog.set_default_size(700, 400)
2644     dialog.vbox.show_all
2645     dialog.run { |response|
2646         iter = treeview.selection.selected
2647         dialog.destroy
2648         if response == Gtk::Dialog::RESPONSE_OK && iter
2649             return model.get_value(iter, 0)
2650         end
2651     }
2652     return nil
2653 end
2654
2655 def show_password_protections
2656     examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2657         child_iter = $albums_iters[xmldir.attributes['path']]
2658         if xmldir.attributes['password-protect']
2659             child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2660             already_protected = true
2661         elsif already_protected
2662             pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2663             if pix
2664                 pix = pix.saturate_and_pixelate(1, true)
2665             end
2666             child_iter[2] = pix
2667         else
2668             child_iter[2] = nil
2669         end
2670         xmldir.elements.each('dir') { |elem|
2671             if !elem.attributes['deleted']
2672                 examine_dir_elem.call(child_iter, elem, already_protected)
2673             end
2674         }
2675     }
2676     examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2677 end
2678
2679 def populate_subalbums_treeview(select_first)
2680     $albums_ts.clear
2681     $autotable.clear
2682     $albums_iters = {}
2683     $subalbums_vb.children.each { |chld|
2684         $subalbums_vb.remove(chld)
2685     }
2686
2687     source = $xmldoc.root.attributes['source']
2688     msg 3, "source: #{source}"
2689
2690     xmldir = $xmldoc.elements['//dir']
2691     if !xmldir || xmldir.attributes['path'] != source
2692         msg 1, _("Corrupted booh file...")
2693         return
2694     end
2695
2696     append_dir_elem = proc { |parent_iter, xmldir|
2697         child_iter = $albums_ts.append(parent_iter)
2698         child_iter[0] = File.basename(xmldir.attributes['path'])
2699         child_iter[1] = xmldir.attributes['path']
2700         $albums_iters[xmldir.attributes['path']] = child_iter
2701         msg 3, "puttin location: #{xmldir.attributes['path']}"
2702         xmldir.elements.each('dir') { |elem|
2703             if !elem.attributes['deleted']
2704                 append_dir_elem.call(child_iter, elem)
2705             end
2706         }
2707     }
2708     append_dir_elem.call(nil, xmldir)
2709     show_password_protections
2710
2711     $albums_tv.expand_all
2712     if select_first
2713         $albums_tv.selection.select_iter($albums_ts.iter_first)
2714     end
2715 end
2716
2717 def select_current_theme
2718     select_theme($xmldoc.root.attributes['theme'],
2719                  $xmldoc.root.attributes['limit-sizes'],
2720                  !$xmldoc.root.attributes['optimize-for-32'].nil?,
2721                  $xmldoc.root.attributes['thumbnails-per-row'])
2722 end
2723
2724 def open_file(filename)
2725
2726     $filename = nil
2727     $modified = false
2728     $current_path = nil   #- invalidate
2729     $modified_pixbufs = {}
2730     $albums_ts.clear
2731     $autotable.clear
2732     $subalbums_vb.children.each { |chld|
2733         $subalbums_vb.remove(chld)
2734     }
2735
2736     if !File.exists?(filename)
2737         return utf8(_("File not found."))
2738     end
2739
2740     begin
2741         $xmldoc = REXML::Document.new File.new(filename)
2742     rescue Exception
2743         $xmldoc = nil
2744     end
2745
2746     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2747         if entry2type(filename).nil?
2748             return utf8(_("Not a booh file!"))
2749         else
2750             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."))
2751         end
2752     end
2753
2754     if !source = $xmldoc.root.attributes['source']
2755         return utf8(_("Corrupted booh file..."))
2756     end
2757
2758     if !dest = $xmldoc.root.attributes['destination']
2759         return utf8(_("Corrupted booh file..."))
2760     end
2761
2762     if !theme = $xmldoc.root.attributes['theme']
2763         return utf8(_("Corrupted booh file..."))
2764     end
2765
2766     if $xmldoc.root.attributes['version'] < '0.8.6'
2767         msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2768         mark_document_as_dirty
2769         if $xmldoc.root.attributes['version'] < '0.8.4'
2770             msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2771             `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2772                 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2773                 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2774                 if old_dest_dir != new_dest_dir
2775                     sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2776                 end
2777                 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2778                     xmldir.elements.each { |element|
2779                         if %w(image video).include?(element.name) && !element.attributes['deleted']
2780                             old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2781                             new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2782                             Dir[old_name + '*'].each { |file|
2783                                 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2784                                 file != new_file and sys("mv '#{file}' '#{new_file}'")
2785                             }
2786                         end
2787                         if element.name == 'dir' && !element.attributes['deleted']
2788                             old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2789                             new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2790                             old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2791                         end
2792                     }
2793                 else
2794                     msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2795                 end
2796             }
2797         end
2798         $xmldoc.root.add_attribute('version', $VERSION)
2799     end
2800
2801     select_current_theme
2802
2803     $filename = filename
2804     $default_size['thumbnails'] =~ /(.*)x(.*)/
2805     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2806     $albums_thumbnail_size =~ /(.*)x(.*)/
2807     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2808
2809     populate_subalbums_treeview(true)
2810
2811     $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
2812     return nil
2813 end
2814
2815 def open_file_user(filename)
2816     result = open_file(filename)
2817     if !result
2818         $config['last-opens'] ||= []
2819         if $config['last-opens'][-1] != utf8(filename)
2820             $config['last-opens'] << utf8(filename)
2821         end
2822         $orig_filename = $filename
2823         tmp = Tempfile.new("boohtemp")
2824         tmp.close!
2825         #- for security
2826         ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2827         ios.close
2828         $tempfiles << $filename << "#{$filename}.backup"
2829     else
2830         $orig_filename = nil
2831     end
2832     return result
2833 end
2834
2835 def open_file_popup
2836     if !ask_save_modifications(utf8(_("Save this album?")),
2837                                utf8(_("Do you want to save the changes to this album?")),
2838                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2839         return
2840     end
2841     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2842                                     nil,
2843                                     Gtk::FileChooser::ACTION_OPEN,
2844                                     nil,
2845                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2846     fc.add_shortcut_folder(File.expand_path("~/.booh"))
2847     fc.set_current_folder(File.expand_path("~/.booh"))
2848     fc.transient_for = $main_window
2849     ok = false
2850     while !ok
2851         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2852             push_mousecursor_wait(fc)
2853             msg = open_file_user(fc.filename)
2854             pop_mousecursor(fc)
2855             if msg
2856                 show_popup(fc, msg)
2857                 ok = false
2858             else
2859                 ok = true
2860             end
2861         else
2862             ok = true
2863         end
2864     end
2865     fc.destroy
2866 end
2867
2868 def open_url(url)
2869     cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2870     msg 2, cmd
2871     system(cmd)
2872 end
2873
2874 def additional_booh_options
2875     options = ''
2876     if $config['mproc']
2877         options += "--mproc #{$config['mproc'].to_i} "
2878     end
2879     options += "--comments-format '#{$config['comments-format']}'"
2880     return options
2881 end
2882
2883 def new_album
2884     if !ask_save_modifications(utf8(_("Save this album?")),
2885                                utf8(_("Do you want to save the changes to this album?")),
2886                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2887         return
2888     end
2889     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2890                              $main_window,
2891                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2892                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2893                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2894     
2895     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2896     tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2897                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2898     tbl.attach(src = Gtk::Entry.new,
2899                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2900     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2901                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2902     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2903                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2904     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2905                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2906     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2907                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2908     tbl.attach(dest = Gtk::Entry.new,
2909                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2910     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2911                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2912     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2913                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2914     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2915                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2916     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2917                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2918
2919     tooltips = Gtk::Tooltips.new
2920     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2921     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2922                          pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2923     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2924                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
2925     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2926     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)
2927     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2928                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2929     nperpage_model = Gtk::ListStore.new(String, String)
2930     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
2931                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
2932     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
2933     nperpagecombo.set_attributes(crt, { :markup => 0 })
2934     iter = nperpage_model.append
2935     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
2936     iter[1] = nil
2937     [ 12, 20, 30, 40, 50 ].each { |v|
2938         iter = nperpage_model.append
2939         iter[0] = iter[1] = v.to_s
2940     }
2941     nperpagecombo.active = 0
2942     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
2943                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
2944     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)
2945     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2946                                    pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
2947     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)
2948
2949     src_nb_calculated_for = ''
2950     src_nb_thread = nil
2951     process_src_nb = proc {
2952         if src.text != src_nb_calculated_for
2953             src_nb_calculated_for = src.text
2954             if src_nb_thread
2955                 Thread.kill(src_nb_thread)
2956                 src_nb_thread = nil
2957             end
2958             if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
2959                 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
2960             else
2961                 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2962                     if File.readable?(from_utf8_safe(src_nb_calculated_for))
2963                         src_nb_thread = Thread.new {
2964                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2965                             total = { 'image' => 0, 'video' => 0, nil => 0 }
2966                             `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2967                                 if File.basename(dir) =~ /^\./
2968                                     next
2969                                 else
2970                                     begin
2971                                         Dir.entries(dir.chomp).each { |file|
2972                                             total[entry2type(file)] += 1
2973                                         }
2974                                     rescue Errno::EACCES, Errno::ENOENT
2975                                     end
2976                                 end
2977                             }
2978                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2979                             src_nb_thread = nil
2980                         }
2981                     else
2982                         src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2983                     end
2984                 else
2985                     src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2986                 end
2987             end
2988         end
2989         true
2990     }
2991     timeout_src_nb = Gtk.timeout_add(100) {
2992         process_src_nb.call
2993     }
2994
2995     src_browse.signal_connect('clicked') {
2996         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2997                                         nil,
2998                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
2999                                         nil,
3000                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3001         fc.transient_for = $main_window
3002         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3003             src.text = utf8(fc.filename)
3004             process_src_nb.call
3005             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3006         end
3007         fc.destroy
3008     }
3009
3010     dest_browse.signal_connect('clicked') {
3011         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3012                                         nil,
3013                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
3014                                         nil,
3015                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3016         fc.transient_for = $main_window
3017         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3018             dest.text = utf8(fc.filename)
3019         end
3020         fc.destroy
3021     }
3022
3023     conf_browse.signal_connect('clicked') {
3024         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3025                                         nil,
3026                                         Gtk::FileChooser::ACTION_SAVE,
3027                                         nil,
3028                                         [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3029         fc.transient_for = $main_window
3030         fc.add_shortcut_folder(File.expand_path("~/.booh"))
3031         fc.set_current_folder(File.expand_path("~/.booh"))
3032         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3033             conf.text = utf8(fc.filename)
3034         end
3035         fc.destroy
3036     }
3037
3038     theme_sizes = []
3039     nperrows = []
3040     recreate_theme_config = proc {
3041         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3042         theme_sizes = []
3043         select_theme(theme_button.label, 'all', optimize432.active?, nil)
3044         $images_size.each { |s|
3045             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3046             if !s['optional']
3047                 cb.active = true
3048             end
3049             tooltips.set_tip(cb, utf8(s['description']), nil)
3050             theme_sizes << { :widget => cb, :value => s['name'] }
3051         }
3052         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3053         tooltips = Gtk::Tooltips.new
3054         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3055         theme_sizes << { :widget => cb, :value => 'original' }
3056         sizes.show_all
3057
3058         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3059         nperrow_group = nil
3060         nperrows = []
3061         $allowed_N_values.each { |n|
3062             if nperrow_group
3063                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3064             else
3065                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3066             end
3067             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3068             if $default_N == n
3069                 rb.active = true
3070             end
3071             nperrows << { :widget => rb, :value => n }
3072         }
3073         nperrowradios.show_all
3074     }
3075     recreate_theme_config.call
3076
3077     theme_button.signal_connect('clicked') {
3078         if newtheme = theme_choose(theme_button.label)
3079             theme_button.label = newtheme
3080             recreate_theme_config.call
3081         end
3082     }
3083
3084     dialog.vbox.add(frame1)
3085     dialog.vbox.add(frame2)
3086     dialog.show_all
3087
3088     keepon = true
3089     ok = true
3090     while keepon
3091         dialog.run { |response|
3092             if response == Gtk::Dialog::RESPONSE_OK
3093                 srcdir = from_utf8_safe(src.text)
3094                 destdir = from_utf8_safe(dest.text)
3095                 confpath = from_utf8_safe(conf.text)
3096                 if src.text != '' && srcdir == ''
3097                     show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3098                     src.grab_focus
3099                 elsif !File.directory?(srcdir)
3100                     show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3101                     src.grab_focus
3102                 elsif dest.text != '' && destdir == ''
3103                     show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3104                     dest.grab_focus
3105                 elsif destdir != make_dest_filename(destdir)
3106                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3107                     dest.grab_focus
3108                 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3109                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
3110                     dest.grab_focus
3111                 elsif File.exists?(destdir) && !File.directory?(destdir)
3112                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3113                     dest.grab_focus
3114                 elsif conf.text == ''
3115                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3116                     conf.grab_focus
3117                 elsif conf.text != '' && confpath == ''
3118                     show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3119                     conf.grab_focus
3120                 elsif File.directory?(confpath)
3121                     show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3122                     conf.grab_focus
3123                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3124                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3125                 else
3126                     system("mkdir '#{destdir}'")
3127                     if !File.directory?(destdir)
3128                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3129                         dest.grab_focus
3130                     else
3131                         keepon = false
3132                     end
3133                 end
3134             else
3135                 keepon = ok = false
3136             end
3137         }
3138     end
3139     if ok
3140         srcdir = from_utf8(src.text)
3141         destdir = from_utf8(dest.text)
3142         configskel = File.expand_path(from_utf8(conf.text))
3143         theme = theme_button.label
3144         sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3145         nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3146         nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3147         opt432 = optimize432.active?
3148         madewith = madewithentry.text.gsub('"', '&quot;').gsub('\'', '&#39;')
3149         indexlink = indexlinkentry.text.gsub('"', '&quot;').gsub('\'', '&#39;')
3150     end
3151     if src_nb_thread
3152         Thread.kill(src_nb_thread)
3153         gtk_thread_flush  #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3154     end
3155     dialog.destroy
3156     Gtk.timeout_remove(timeout_src_nb)
3157
3158     if ok
3159         call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3160                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3161                      (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3162                      "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3163                      utf8(_("Please wait while scanning source directory...")),
3164                      'full scan',
3165                      { :closure_after => proc { open_file_user(configskel) } })
3166     end
3167 end
3168
3169 def properties
3170     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3171                              $main_window,
3172                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3173                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3174                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3175     
3176     source = $xmldoc.root.attributes['source']
3177     dest = $xmldoc.root.attributes['destination']
3178     theme = $xmldoc.root.attributes['theme']
3179     opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3180     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3181     nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3182     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3183     if limit_sizes
3184         limit_sizes = limit_sizes.split(/,/)
3185     end
3186     madewith = $xmldoc.root.attributes['made-with'].gsub('&#39;', '\'')
3187     indexlink = $xmldoc.root.attributes['index-link'].gsub('&#39;', '\'')
3188
3189     tooltips = Gtk::Tooltips.new
3190     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3191     tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3192                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3193     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3194                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3195     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3196                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3197     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3198                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3199     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3200                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3201     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3202                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3203
3204     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3205     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3206                                    pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3207     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3208                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3209     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3210     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)
3211     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3212                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3213     nperpage_model = Gtk::ListStore.new(String, String)
3214     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3215                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3216     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3217     nperpagecombo.set_attributes(crt, { :markup => 0 })
3218     iter = nperpage_model.append
3219     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3220     iter[1] = nil
3221     [ 12, 20, 30, 40, 50 ].each { |v|
3222         iter = nperpage_model.append
3223         iter[0] = iter[1] = v.to_s
3224         if nperpage && nperpage == v.to_s
3225             nperpagecombo.active_iter = iter
3226         end
3227     }
3228     if nperpagecombo.active_iter.nil?
3229         nperpagecombo.active = 0
3230     end
3231
3232     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3233                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3234     if indexlink
3235         indexlinkentry.text = indexlink
3236     end
3237     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)
3238     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3239                                    pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3240     if madewith
3241         madewithentry.text = madewith
3242     end
3243     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)
3244
3245     theme_sizes = []
3246     nperrows = []
3247     recreate_theme_config = proc {
3248         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3249         theme_sizes = []
3250         select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3251
3252         $images_size.each { |s|
3253             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3254             if limit_sizes
3255                 if limit_sizes.include?(s['name'])
3256                     cb.active = true
3257                 end
3258             else
3259                 if !s['optional']
3260                     cb.active = true
3261                 end
3262             end
3263             tooltips.set_tip(cb, utf8(s['description']), nil)
3264             theme_sizes << { :widget => cb, :value => s['name'] }
3265         }
3266         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3267         tooltips = Gtk::Tooltips.new
3268         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3269         if limit_sizes && limit_sizes.include?('original')
3270             cb.active = true
3271         end
3272         theme_sizes << { :widget => cb, :value => 'original' }
3273         sizes.show_all
3274
3275         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3276         nperrow_group = nil
3277         nperrows = []
3278         $allowed_N_values.each { |n|
3279             if nperrow_group
3280                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3281             else
3282                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3283             end
3284             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3285             nperrowradios.add(Gtk::Label.new('  '))
3286             if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3287                 rb.active = true
3288             end
3289             nperrows << { :widget => rb, :value => n.to_s }
3290         }
3291         nperrowradios.show_all
3292     }
3293     recreate_theme_config.call
3294
3295     theme_button.signal_connect('clicked') {
3296         if newtheme = theme_choose(theme_button.label)
3297             limit_sizes = nil
3298             nperrow = nil
3299             theme_button.label = newtheme
3300             recreate_theme_config.call
3301         end
3302     }
3303
3304     dialog.vbox.add(frame1)
3305     dialog.vbox.add(frame2)
3306     dialog.show_all
3307
3308     keepon = true
3309     ok = true
3310     while keepon
3311         dialog.run { |response|
3312             if response == Gtk::Dialog::RESPONSE_OK
3313                 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3314                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3315                 else
3316                     keepon = false
3317                 end
3318             else
3319                 keepon = ok = false
3320             end
3321         }
3322     end
3323     save_theme = theme_button.label
3324     save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3325     save_opt432 = optimize432.active?
3326     save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3327     save_nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3328     save_madewith = madewithentry.text.gsub('"', '&quot;').gsub('\'', '&#39;')
3329     save_indexlink = indexlinkentry.text.gsub('"', '&quot;').gsub('\'', '&#39;')
3330     dialog.destroy
3331
3332     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)
3333         mark_document_as_dirty
3334         save_current_file
3335         call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3336                      "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3337                      (save_nperpage ? "--thumbnails-per-page #{save_nperpage} " : '') +
3338                      "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3339                      utf8(_("Please wait while scanning source directory...")),
3340                      'full scan',
3341                      { :closure_after => proc {
3342                              open_file($filename)
3343                              $modified = true
3344                          } })
3345     else
3346         #- select_theme merges global variables, need to return to current choices
3347         select_current_theme
3348     end
3349 end
3350
3351 def merge_current
3352     save_current_file
3353
3354     sel = $albums_tv.selection.selected_rows
3355
3356     call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3357                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
3358                  utf8(_("Please wait while scanning source directory...")),
3359                  'one dir scan',
3360                  { :closure_after => proc {
3361                          open_file($filename)
3362                          $albums_tv.selection.select_path(sel[0])
3363                          $modified = true
3364                      } })
3365 end
3366
3367 def merge_newsubs
3368     save_current_file
3369
3370     sel = $albums_tv.selection.selected_rows
3371
3372     call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3373                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
3374                  utf8(_("Please wait while scanning source directory...")),
3375                  'subdirs scan',
3376                  { :closure_after => proc {
3377                          open_file($filename)
3378                          $albums_tv.selection.select_path(sel[0])
3379                          $modified = true
3380                      } })
3381 end
3382
3383 def merge
3384     save_current_file
3385
3386     theme = $xmldoc.root.attributes['theme']
3387     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3388     if limit_sizes
3389         limit_sizes = "--sizes #{limit_sizes}"
3390     end
3391     call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3392                  "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3393                  utf8(_("Please wait while scanning source directory...")),
3394                  'full scan',
3395                  { :closure_after => proc {
3396                          open_file($filename)
3397                          $modified = true
3398                      } })
3399 end
3400
3401 def save_as_do
3402     fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3403                                     nil,
3404                                     Gtk::FileChooser::ACTION_SAVE,
3405                                     nil,
3406                                     [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3407     fc.transient_for = $main_window
3408     fc.add_shortcut_folder(File.expand_path("~/.booh"))
3409     fc.set_current_folder(File.expand_path("~/.booh"))
3410     fc.filename = $orig_filename
3411     if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3412         $orig_filename = fc.filename
3413         if ! save_current_file_user
3414             fc.destroy
3415             return save_as_do
3416         end
3417         $config['last-opens'] ||= []
3418         $config['last-opens'] << $orig_filename
3419     end
3420     fc.destroy
3421 end
3422
3423 def preferences
3424     dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3425                              $main_window,
3426                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3427                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3428                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3429
3430     dialog.vbox.add(notebook = Gtk::Notebook.new)
3431     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3432     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3433                0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3434     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)),
3435                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3436     tooltips = Gtk::Tooltips.new
3437     tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3438 for example: /usr/bin/mplayer %f")), nil)
3439     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3440                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3441     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3442                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3443     tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3444 for example: /usr/bin/gimp-remote %f")), nil)
3445     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3446                0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3447     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3448                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3449     tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3450 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3451     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3452                0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3453     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)),
3454                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3455     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)
3456     tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3457                0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3458     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)
3459     tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3460                0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3461     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)
3462
3463     smp_check.signal_connect('toggled') {
3464         if smp_check.active?
3465             smp_hbox.sensitive = true
3466         else
3467             smp_hbox.sensitive = false
3468         end
3469     }
3470     if $config['mproc']
3471         smp_check.active = true
3472         smp_spin.value = $config['mproc'].to_i
3473     end
3474     nogestures_check.active = $config['nogestures']
3475     deleteondisk_check.active = $config['deleteondisk']
3476
3477     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3478     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3479                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3480     tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3481                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3482     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3483                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3484     tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3485                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3486     tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3487                2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3488     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)
3489     commentsformat_help.signal_connect('clicked') {
3490         show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3491 hence you should look at ImageMagick/identify documentation for the most    
3492 accurate and up-to-date documentation. Last time I checked, documentation
3493 was:
3494
3495 Print information about the image in a format of your choosing. You can
3496 include the image filename, type, width, height, Exif data, or other image
3497 attributes by embedding special format characters:      &nbs