88c4dd30090bc124a3d6bd8b86dc15e3be904857
[booh] / bin / booh
1 #! /usr/bin/ruby
2 #
3 #                         *  BOOH  *
4 #
5 # A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
6 #
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
11 #
12 #
13 # Copyright (c) 2004-2006 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
14 #
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21
22 require 'getoptlong'
23 require 'tempfile'
24 require 'thread'
25
26 require 'gtk2'
27 require 'booh/gtkadds'
28 require 'booh/GtkAutoTable'
29
30 require 'gettext'
31 include GetText
32 bindtextdomain("booh")
33
34 require 'rexml/document'
35 include REXML
36
37 require 'booh/booh-lib'
38 include Booh
39 require 'booh/UndoHandler'
40
41
42 #- options
43 $options = [
44     [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       _("Get help message") ],
45
46     [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
47 ]
48
49 #- default values for some globals 
50 $xmldir = nil
51 $modified = false
52 $current_cursor = nil
53 $ignore_videos = false
54 $button1_pressed_autotable = false
55 $generated_outofline = false
56
57 def usage
58     puts _("Usage: %s [OPTION]...") % File.basename($0)
59     $options.each { |ary|
60         printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
61     }
62 end
63
64 def handle_options
65     parser = GetoptLong.new
66     parser.set_options(*$options.collect { |ary| ary[0..2] })
67     begin
68         parser.each_option do |name, arg|
69             case name
70             when '--help'
71                 usage
72                 exit(0)
73
74             when '--verbose-level'
75                 $verbose_level = arg.to_i
76
77             end
78         end
79     rescue
80         puts $!
81         usage
82         exit(1)
83     end
84 end
85
86 def read_config
87     $config = {}
88     $config_file = File.expand_path('~/.booh-gui-rc')
89     if File.readable?($config_file)
90         $xmldoc = REXML::Document.new(File.new($config_file))
91         $xmldoc.root.elements.each { |element|
92             txt = element.get_text
93             if txt
94                 if txt.value =~ /~~~/ || element.name == 'last-opens'
95                     $config[element.name] = txt.value.split(/~~~/)
96                 else
97                     $config[element.name] = txt.value
98                 end
99             elsif element.elements.size == 0
100                 $config[element.name] = ''
101             else
102                 $config[element.name] = {}
103                 element.each { |chld|
104                     txt = chld.get_text
105                     $config[element.name][chld.name] = txt ? txt.value : nil
106                 }
107             end
108         }
109     end
110     $config['video-viewer'] ||= '/usr/bin/mplayer %f'
111     $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
112     $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
113     $config['comments-format'] ||= '%t'
114     if !FileTest.directory?(File.expand_path('~/.booh'))
115         system("mkdir ~/.booh")
116     end
117
118     $tempfiles = []
119     $todelete = []
120 end
121
122 def check_config
123     if !system("which convert >/dev/null 2>/dev/null")
124         show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
125 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
126         exit 1
127     end
128     if !system("which identify >/dev/null 2>/dev/null")
129         show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
130 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
131     end
132     missing = %w(transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
133     if missing != []
134         show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
135     end
136
137     viewer_binary = $config['video-viewer'].split.first
138     if viewer_binary && !File.executable?(viewer_binary)
139         show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
140 You should fix this in Edit/Preferences so that you can view videos.
141
142 Problem was: '%s' is not an executable file.
143 Hint: don't forget to specify the full path to the executable,
144 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
145     end
146     image_editor_binary = $config['image-editor'].split.first
147     if image_editor_binary && !File.executable?(image_editor_binary)
148         show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
149 You should fix this in Edit/Preferences so that you can edit images externally.
150
151 Problem was: '%s' is not an executable file.
152 Hint: don't forget to specify the full path to the executable,
153 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
154     end
155     browser_binary = $config['browser'].split.first
156     if browser_binary && !File.executable?(browser_binary)
157         show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
158 You should fix this in Edit/Preferences so that you can open URLs.
159
160 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
161     end
162 end
163
164 def write_config
165     if $config['last-opens'] && $config['last-opens'].size > 10
166         $config['last-opens'] = $config['last-opens'][-10, 10]
167     end
168
169     ios = File.open($config_file, "w")
170     $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
171     $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
172     $config.each_pair { |key, value|
173         elem = $xmldoc.root.add_element key
174         if value.is_a? Hash
175             $config[key].each_pair { |subkey, subvalue|
176                 subelem = elem.add_element subkey
177                 subelem.add_text subvalue.to_s
178             }
179         elsif value.is_a? Array
180             elem.add_text value.join('~~~')
181         else
182             if !value
183                 elem.remove
184             else
185                 elem.add_text value.to_s
186             end
187         end
188     }
189     $xmldoc.write(ios, 0)
190     ios.close
191
192     $tempfiles.each { |f|
193         system("rm -f #{f}")
194     }
195 end
196
197 def set_mousecursor(what, *widget)
198     cursor = what.nil? ? nil : Gdk::Cursor.new(what)
199     if widget[0] && widget[0].window
200         widget[0].window.cursor = cursor
201     end
202     if $main_window && $main_window.window
203         $main_window.window.cursor = cursor
204     end
205     $current_cursor = what
206 end
207 def set_mousecursor_wait(*widget)
208     gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
209     if Thread.current == Thread.main
210         Gtk.main_iteration while Gtk.events_pending?
211     end
212 end
213 def set_mousecursor_normal(*widget)
214     gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
215 end
216 def push_mousecursor_wait(*widget)
217     if $current_cursor != Gdk::Cursor::WATCH
218         $save_cursor = $current_cursor
219         gtk_thread_protect { set_mousecursor_wait(*widget) }
220     end
221 end
222 def pop_mousecursor(*widget)
223     gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
224 end
225
226 def current_dest_dir
227     source = $xmldoc.root.attributes['source']
228     dest = $xmldoc.root.attributes['destination']
229     return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
230 end
231
232 def full_src_dir_to_rel(path, source)
233     return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
234 end
235
236 def build_full_dest_filename(filename)
237     return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
238 end
239
240 def save_undo(name, closure, *params)
241     UndoHandler.save_undo(name, closure, [ *params ])
242     $undo_tb.sensitive = $undo_mb.sensitive = true
243     $redo_tb.sensitive = $redo_mb.sensitive = false
244 end
245
246 def view_element(filename, closures)
247     if entry2type(filename) == 'video'
248         cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
249         msg 2, cmd
250         system(cmd)
251         return
252     end
253
254     w = Gtk::Window.new.set_title(filename)
255
256     msg 3, "filename: #{filename}"
257     dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
258     #- typically this file won't exist in case of videos; try with the largest thumbnail around
259     if !File.exists?(dest_img)
260         if entry2type(filename) == 'video'
261             alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
262             if not alternatives.empty?
263                 dest_img = alternatives[-1]
264             end
265         else
266             push_mousecursor_wait
267             gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
268             pop_mousecursor
269             if !File.exists?(dest_img)
270                 msg 2, _("Could not generate fullscreen thumbnail!")
271                 return
272                 end
273         end
274     end
275     evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(Gtk::Image.new(dest_img)).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
276     evt.signal_connect('button-press-event') { |this, event|
277         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
278             $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
279         end
280         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
281             menu = Gtk::Menu.new
282             menu.append(delete_item  = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
283             delete_item.signal_connect('activate') {
284                 w.destroy
285                 closures[:delete].call
286             }
287             menu.show_all
288             menu.popup(nil, nil, event.button, event.time)
289         end
290     }
291     evt.signal_connect('button-release-event') { |this, event|
292         if $gesture_press
293             if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
294                 msg 3, "gesture delete: click-drag right button to the bottom"
295                 w.destroy
296                 closures[:delete].call
297                 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
298             end
299         end
300     }
301     tooltips = Gtk::Tooltips.new
302     tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
303
304     w.signal_connect('key-press-event') { |w,event|
305         if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
306             w.destroy
307             closures[:delete].call
308         end
309     }
310
311     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
312     b.signal_connect('clicked') { w.destroy }
313
314     vb = Gtk::VBox.new
315     vb.pack_start(evt, false, false)
316     vb.pack_end(bottom, false, false)
317
318     w.add(vb)
319     w.signal_connect('delete-event') { w.destroy }
320     w.window_position = Gtk::Window::POS_CENTER
321     w.show_all
322 end
323
324 def scroll_upper(scrolledwindow, ypos_top)
325     newval = scrolledwindow.vadjustment.value -
326         ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
327     if newval < scrolledwindow.vadjustment.lower
328         newval = scrolledwindow.vadjustment.lower
329     end
330     scrolledwindow.vadjustment.value = newval
331 end
332
333 def scroll_lower(scrolledwindow, ypos_bottom)
334     newval = scrolledwindow.vadjustment.value +
335         ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
336     if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
337         newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
338     end
339     scrolledwindow.vadjustment.value = newval
340 end
341
342 def autoscroll_if_needed(scrolledwindow, image, textview)
343     #- autoscroll if cursor or image is not visible, if possible
344     if image && image.window || textview.window
345         ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
346         ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
347         current_miny_visible = scrolledwindow.vadjustment.value
348         current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
349         if ypos_top < current_miny_visible
350             scroll_upper(scrolledwindow, ypos_top)
351         elsif ypos_bottom > current_maxy_visible
352             scroll_lower(scrolledwindow, ypos_bottom)
353         end
354     end
355 end
356
357 def create_editzone(scrolledwindow, pagenum, image)
358     frame = Gtk::Frame.new
359     frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
360     frame.set_shadow_type(Gtk::SHADOW_IN)
361     textview.signal_connect('key-press-event') { |w, event|
362         textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
363         if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
364             scrolledwindow.signal_emit('key-press-event', event)
365         end
366         if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
367            event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
368             if event.keyval == Gdk::Keyval::GDK_Up
369                 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
370                     scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
371                 else
372                     scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
373                 end
374             else
375                 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
376                     scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
377                 else
378                     scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
379                 end
380             end
381         end
382         false  #- propagate
383     }
384
385     candidate_undo_text = nil
386     textview.signal_connect('focus-in-event') { |w, event|
387         textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
388         candidate_undo_text = textview.buffer.text
389         false  #- propagate
390     }
391
392     textview.signal_connect('key-release-event') { |w, event|
393         if candidate_undo_text && candidate_undo_text != textview.buffer.text
394             $modified = true
395             save_undo(_("text edit"),
396                       proc { |text|
397                           save_text = textview.buffer.text
398                           textview.buffer.text = text
399                           textview.grab_focus
400                           $notebook.set_page(pagenum)
401                           proc {
402                               textview.buffer.text = save_text
403                               textview.grab_focus
404                               $notebook.set_page(pagenum)
405                           }
406                       }, candidate_undo_text)
407             candidate_undo_text = nil
408         end
409
410         if event.state != 0 || ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
411             autoscroll_if_needed(scrolledwindow, image, textview)
412         end
413         false  #- propagate
414     }
415
416     return [ frame, textview ]
417 end
418
419 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
420
421     if !$modified_pixbufs[thumbnail_img]
422         $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
423     elsif !$modified_pixbufs[thumbnail_img][:orig]
424         $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
425     end
426
427     pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
428
429     #- rotate
430     if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
431         pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
432         msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
433         if pixbuf.height > desired_y
434             pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
435         elsif pixbuf.width < desired_x && pixbuf.height < desired_y
436             pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
437         end
438     end
439
440     #- fix white balance
441     if $modified_pixbufs[thumbnail_img][:whitebalance]
442         pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
443     end
444
445     #- fix gamma correction
446     if $modified_pixbufs[thumbnail_img][:gammacorrect]
447         pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
448     end
449
450     img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
451 end
452
453 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
454     $modified = true
455
456     #- update rotate attribute
457     xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
458
459     $modified_pixbufs[thumbnail_img] ||= {}
460     $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
461     msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
462
463     update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
464 end
465
466 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
467     $modified = true
468
469     rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
470
471     save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
472               proc { |angle|
473                   rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
474                   $notebook.set_page(attributes_prefix != '' ? 0 : 1)
475                   proc {
476                       rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
477                       $notebook.set_page(0)
478                       $notebook.set_page(attributes_prefix != '' ? 0 : 1)
479                   }
480               }, -angle)
481 end
482
483 def color_swap(xmldir, attributes_prefix)
484     $modified = true
485     if xmldir.attributes["#{attributes_prefix}color-swap"]
486         xmldir.delete_attribute("#{attributes_prefix}color-swap")
487     else
488         xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
489     end
490 end
491
492 def enhance(xmldir, attributes_prefix)
493     $modified = true
494     if xmldir.attributes["#{attributes_prefix}enhance"]
495         xmldir.delete_attribute("#{attributes_prefix}enhance")
496     else
497         xmldir.add_attribute("#{attributes_prefix}enhance", '1')
498     end
499 end
500
501 def change_frame_offset(xmldir, attributes_prefix, value)
502     $modified = true
503     xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
504 end
505
506 def ask_new_frame_offset(xmldir, attributes_prefix)
507     if xmldir
508         value = xmldir.attributes["#{attributes_prefix}frame-offset"]
509     else
510         value = ''
511     end
512
513     dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
514                              $main_window,
515                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
516                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
517                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
518
519     lbl = Gtk::Label.new
520     lbl.markup = utf8(
521 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
522 from. There are approximately 25 frames per second in a video.
523 "))
524     dialog.vbox.add(lbl)
525     dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
526     entry.signal_connect('key-press-event') { |w, event|
527         if event.keyval == Gdk::Keyval::GDK_Return
528             dialog.response(Gtk::Dialog::RESPONSE_OK)
529             true
530         elsif event.keyval == Gdk::Keyval::GDK_Escape
531             dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
532             true
533         else
534             false  #- propagate if needed
535         end
536     }
537
538     dialog.window_position = Gtk::Window::POS_MOUSE
539     dialog.show_all
540
541     dialog.run { |response|
542         newval = entry.text
543         dialog.destroy
544         if response == Gtk::Dialog::RESPONSE_OK
545             $modified = true
546             msg 3, "changing frame offset to #{newval}"
547             return { :old => value, :new => newval }
548         else
549             return nil
550         end
551     }
552 end
553
554 def change_pano_amount(xmldir, attributes_prefix, value)
555     $modified = true
556     if value.nil?
557         xmldir.delete_attribute("#{attributes_prefix}pano-amount")
558     else
559         xmldir.add_attribute("#{attributes_prefix}pano-amount", value)
560     end
561 end
562
563 def ask_new_pano_amount(xmldir, attributes_prefix)
564     if xmldir
565         value = xmldir.attributes["#{attributes_prefix}pano-amount"]
566     else
567         value = nil
568     end
569
570     dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
571                              $main_window,
572                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
573                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
574                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
575
576     lbl = Gtk::Label.new
577     lbl.markup = utf8(
578 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
579 of this panorama image compared to other regular images. For example, if the panorama
580 was taken out of four photos on one row, counting the necessary overlap, the width of
581 this panorama image should probably be roughly three times the width of regular images.
582
583 With this information, booh will be able to generate panorama thumbnails looking
584 the right 'size'.
585 "))
586     dialog.vbox.add(lbl)
587     dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("none (not a panorama image)")))).
588                                                                          add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
589                                                                          add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
590                                                                          add(Gtk::Label.new(utf8(_("times the width of other images"))))))
591     dialog.window_position = Gtk::Window::POS_MOUSE
592     dialog.show_all
593     if value
594         spin.value = value.to_f
595         rb_yes.active = true
596         spin.grab_focus
597     else
598         rb_no.active = true
599     end
600
601     dialog.run { |response|
602         if rb_no.active?
603             newval = nil
604         else
605             newval = spin.value.to_f
606         end
607         dialog.destroy
608         if response == Gtk::Dialog::RESPONSE_OK
609             $modified = true
610             msg 3, "changing panorama amount to #{newval}"
611             return { :old => value, :new => newval }
612         else
613             return nil
614         end
615     }
616 end
617
618 def change_whitebalance(xmlelem, attributes_prefix, value)
619     $modified = true
620     xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
621 end
622
623 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
624
625     #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
626     if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
627         save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
628         xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
629         save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
630         xmlelem.delete_attribute("#{attributes_prefix}white-balance")
631         destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
632         gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
633                                 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
634         $modified_pixbufs[thumbnail_img] ||= {}
635         $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
636         xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
637         if save_gammacorrect
638             xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
639             $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
640         end
641         $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
642     end
643
644     $modified_pixbufs[thumbnail_img] ||= {}
645     $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
646
647     update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
648 end
649
650 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
651     #- init $modified_pixbufs correctly
652 #    update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
653
654     value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
655
656     dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
657                              $main_window,
658                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
659                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
660                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
661
662     lbl = Gtk::Label.new
663     lbl.markup = utf8(
664 _("You can fix the <b>white balance</b> of the image, if your image is too blue
665 or too yellow because your camera didn't detect the light correctly. Drag the
666 slider below the image to the left for more blue, to the right for more yellow.
667 "))
668     dialog.vbox.add(lbl)
669     if img_
670         dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
671     end
672     dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
673     
674     dialog.window_position = Gtk::Window::POS_MOUSE
675     dialog.show_all
676
677     lastval = nil
678     timeout = Gtk.timeout_add(100) {
679         if hs.value != lastval
680             lastval = hs.value
681             if img_
682                 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
683             end
684         end
685         true
686     }
687
688     dialog.run { |response|
689         Gtk.timeout_remove(timeout)
690         if response == Gtk::Dialog::RESPONSE_OK
691             $modified = true
692             newval = hs.value.to_s
693             msg 3, "changing white balance to #{newval}"
694             dialog.destroy
695             return { :old => value, :new => newval }
696         else
697             if thumbnail_img
698                 $modified_pixbufs[thumbnail_img] ||= {}
699                 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
700                 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
701             end
702             dialog.destroy
703             return nil
704         end
705     }
706 end
707
708 def change_gammacorrect(xmlelem, attributes_prefix, value)
709     $modified = true
710     xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
711 end
712
713 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
714
715     #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
716     if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
717         save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
718         xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
719         save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
720         xmlelem.delete_attribute("#{attributes_prefix}white-balance")
721         destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
722         gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
723                                 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
724         $modified_pixbufs[thumbnail_img] ||= {}
725         $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
726         xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
727         if save_whitebalance
728             xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
729             $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
730         end
731         $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
732     end
733
734     $modified_pixbufs[thumbnail_img] ||= {}
735     $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
736
737     update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
738 end
739
740 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
741     #- init $modified_pixbufs correctly
742 #    update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
743
744     value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
745
746     dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
747                              $main_window,
748                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
749                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
750                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
751
752     lbl = Gtk::Label.new
753     lbl.markup = utf8(
754 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
755 or too bright. Drag the slider below the image.
756 "))
757     dialog.vbox.add(lbl)
758     if img_
759         dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
760     end
761     dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
762     
763     dialog.window_position = Gtk::Window::POS_MOUSE
764     dialog.show_all
765
766     lastval = nil
767     timeout = Gtk.timeout_add(100) {
768         if hs.value != lastval
769             lastval = hs.value
770             if img_
771                 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
772             end
773         end
774         true
775     }
776
777     dialog.run { |response|
778         Gtk.timeout_remove(timeout)
779         if response == Gtk::Dialog::RESPONSE_OK
780             $modified = true
781             newval = hs.value.to_s
782             msg 3, "gamma correction to #{newval}"
783             dialog.destroy
784             return { :old => value, :new => newval }
785         else
786             if thumbnail_img
787                 $modified_pixbufs[thumbnail_img] ||= {}
788                 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
789                 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
790             end
791             dialog.destroy
792             return nil
793         end
794     }
795 end
796
797 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
798     system("rm -f '#{destfile}'")
799     #- type can be 'element' or 'subdir'
800     if type == 'element'
801         gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
802     else
803         gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
804     end
805 end
806
807 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
808     Thread.new {
809         push_mousecursor_wait
810         gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
811         gtk_thread_protect {
812             img.set(destfile)
813             $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
814         }
815         pop_mousecursor
816     }
817 end
818
819 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
820     distribute_multiple_call = Proc.new { |action, arg|
821         $selected_elements.each_key { |path|
822             $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
823         }
824         if possible_actions[:can_multiple] && $selected_elements.length > 0
825             UndoHandler.begin_batch
826             $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
827             UndoHandler.end_batch
828         else
829             closures[action].call(arg)
830         end
831         $selected_elements = {}
832     }
833     menu = Gtk::Menu.new
834     if optionals.include?('change_image')
835         menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
836         changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
837         changeimg.signal_connect('activate') { closures[:change].call }
838         menu.append(Gtk::SeparatorMenuItem.new)
839     end
840     if !possible_actions[:can_multiple] || $selected_elements.length == 0
841         if closures[:view]
842             if type == 'image'
843                 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
844                 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
845                 view.signal_connect('activate') { closures[:view].call }
846             else
847                 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
848                 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
849                 view.signal_connect('activate') { closures[:view].call }
850                 menu.append(Gtk::SeparatorMenuItem.new)
851             end
852         end
853         if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
854             menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
855             exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
856             exif.signal_connect('activate') { show_popup($main_window,
857                                                          utf8(`identify -format "%[EXIF:*]" #{fullpath}`.sub(/MakerNote.*\n/, '')),
858                                                          { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
859             menu.append(Gtk::SeparatorMenuItem.new)
860         end
861     end
862     menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
863     r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
864     r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
865     menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
866     r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
867     r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
868     if !possible_actions[:can_multiple] || $selected_elements.length == 0
869         menu.append(Gtk::SeparatorMenuItem.new)
870         if !possible_actions[:forbid_left]
871             menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
872             moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
873             moveleft.signal_connect('activate') { closures[:move].call('left') }
874             if !possible_actions[:can_left]
875                 moveleft.sensitive = false
876             end
877         end
878         if !possible_actions[:forbid_right]
879             menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
880             moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
881             moveright.signal_connect('activate') { closures[:move].call('right') }
882             if !possible_actions[:can_right]
883                 moveright.sensitive = false
884             end
885         end
886         if optionals.include?('move_top')
887             menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
888             movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
889             movetop.signal_connect('activate') { closures[:move].call('top') }
890             if !possible_actions[:can_top]
891                 movetop.sensitive = false
892             end
893         end
894         menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
895         moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
896         moveup.signal_connect('activate') { closures[:move].call('up') }
897         if !possible_actions[:can_up]
898             moveup.sensitive = false
899         end
900         menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
901         movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
902         movedown.signal_connect('activate') { closures[:move].call('down') }
903         if !possible_actions[:can_down]
904             movedown.sensitive = false
905         end
906         if optionals.include?('move_bottom')
907             menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
908             movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
909             movebottom.signal_connect('activate') { closures[:move].call('bottom') }
910             if !possible_actions[:can_bottom]
911                 movebottom.sensitive = false
912             end
913         end
914     end
915     if type == 'video'
916         if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
917             menu.append(Gtk::SeparatorMenuItem.new)
918             menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
919             color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
920             color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
921             menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
922             flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
923             flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
924             menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
925             frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
926             frame_offset.signal_connect('activate') {
927                 if possible_actions[:can_multiple] && $selected_elements.length > 0
928                     if values = ask_new_frame_offset(nil, '')
929                         distribute_multiple_call.call(:frame_offset, values)
930                     end
931                 else
932                     closures[:frame_offset].call
933                 end
934             }
935         end
936     end
937     menu.append(               Gtk::SeparatorMenuItem.new)
938     menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
939     gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
940     gammacorrect.signal_connect('activate') { 
941         if possible_actions[:can_multiple] && $selected_elements.length > 0
942             if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
943                 distribute_multiple_call.call(:gammacorrect, values)
944             end
945         else
946             closures[:gammacorrect].call
947         end
948     }
949     menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
950     whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
951     whitebalance.signal_connect('activate') { 
952         if possible_actions[:can_multiple] && $selected_elements.length > 0
953             if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
954                 distribute_multiple_call.call(:whitebalance, values)
955             end
956         else
957             closures[:whitebalance].call
958         end
959     }
960     if !possible_actions[:can_multiple] || $selected_elements.length == 0
961         menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
962                                                                                                              _("Enhance constrast"))))
963     else
964         menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
965     end
966     enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
967     enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
968     if type == 'image' && possible_actions[:can_panorama]
969         menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
970         panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
971         panorama.signal_connect('activate') {
972             if possible_actions[:can_multiple] && $selected_elements.length > 0
973                 if values = ask_new_pano_amount(nil, '')
974                     distribute_multiple_call.call(:pano, values)
975                 end
976             else
977                 distribute_multiple_call.call(:pano)
978             end
979        }
980     end
981     menu.append(               Gtk::SeparatorMenuItem.new)
982     if optionals.include?('delete')
983         menu.append(cut_item     = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
984         cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
985         if !possible_actions[:can_multiple] || $selected_elements.length == 0
986             menu.append(paste_item   = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
987             paste_item.signal_connect('activate') { closures[:paste].call }
988             menu.append(clear_item   = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
989             clear_item.signal_connect('activate') { $cuts = [] }
990             if $cuts.size == 0
991                 paste_item.sensitive = clear_item.sensitive = false
992             end
993         end
994         menu.append(               Gtk::SeparatorMenuItem.new)
995     end
996     if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
997         menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
998         editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
999         editexternally.signal_connect('activate') {
1000             cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1001             msg 2, cmd
1002             system(cmd)
1003         }
1004     end
1005     menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1006     refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1007     if optionals.include?('delete')
1008         menu.append(delete_item  = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1009         delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1010     end
1011     menu.show_all
1012     menu.popup(nil, nil, event.button, event.time)
1013 end
1014
1015 def delete_current_subalbum
1016     $modified = true
1017     sel = $albums_tv.selection.selected_rows
1018     $xmldir.elements.each { |e|
1019         if e.name == 'image' || e.name == 'video'
1020             e.add_attribute('deleted', 'true')
1021         end
1022     }
1023     #- branch if we have a non deleted subalbum
1024     if $xmldir.child_byname_notattr('dir', 'deleted')
1025         $xmldir.delete_attribute('thumbnails-caption')
1026         $xmldir.delete_attribute('thumbnails-captionfile')
1027     else
1028         $xmldir.add_attribute('deleted', 'true')
1029         moveup = $xmldir
1030         while moveup.parent.name == 'dir'
1031             moveup = moveup.parent
1032             if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1033                 moveup.add_attribute('deleted', 'true')
1034             else
1035                 break
1036             end
1037         end
1038         sel[0].up!
1039     end
1040     save_changes('forced')
1041     populate_subalbums_treeview(false)
1042     $albums_tv.selection.select_path(sel[0])
1043 end
1044
1045 def restore_deleted
1046     $modified = true
1047     save_changes
1048     $current_path = nil  #- prevent save_changes from being rerun again
1049     sel = $albums_tv.selection.selected_rows
1050     restore_one = proc { |xmldir|
1051         xmldir.elements.each { |e|
1052             if e.name == 'dir' && e.attributes['deleted']
1053                 restore_one.call(e)
1054             end
1055             e.delete_attribute('deleted')
1056         }
1057     }
1058     restore_one.call($xmldir)
1059     populate_subalbums_treeview(false)
1060     $albums_tv.selection.select_path(sel[0])
1061 end
1062
1063 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1064
1065     img = nil
1066     frame1 = Gtk::Frame.new
1067     fullpath = from_utf8("#{$current_path}/#{filename}")
1068
1069     my_gen_real_thumbnail = proc {
1070         gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1071     }
1072
1073     if type == 'video'
1074         pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1075         frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1076                                  pack_start(img = Gtk::Image.new).
1077                                  pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1078         px, mask = pxb.render_pixmap_and_mask
1079         da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1080         da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1081     else
1082         frame1.add(img = Gtk::Image.new)
1083     end
1084
1085     #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1086     if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1087         my_gen_real_thumbnail.call
1088     else
1089         img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1090     end
1091
1092     evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1093
1094     tooltips = Gtk::Tooltips.new
1095     tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1096     tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1097
1098     frame2, textview = create_editzone($autotable_sw, 1, img)
1099     textview.buffer.text = caption
1100     textview.set_justification(Gtk::Justification::CENTER)
1101
1102     vbox = Gtk::VBox.new(false, 5)
1103     vbox.pack_start(evtbox, false, false)
1104     vbox.pack_start(frame2, false, false)
1105     autotable.append(vbox, filename)
1106
1107     #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1108     $vbox2widgets[vbox] = { :textview => textview, :image => img }
1109
1110     #- to be able to find widgets by name
1111     $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1112
1113     cleanup_all_thumbnails = proc {
1114         #- remove out of sync images
1115         dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1116         for sizeobj in $images_size
1117             system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
1118         end
1119
1120     }
1121
1122     refresh = proc {
1123         cleanup_all_thumbnails.call
1124         my_gen_real_thumbnail.call
1125     }
1126  
1127     rotate_and_cleanup = proc { |angle|
1128         rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1129         cleanup_all_thumbnails.call
1130     }
1131
1132     move = proc { |direction|
1133         do_method = "move_#{direction}"
1134         undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1135         perform = proc {
1136             done = autotable.method(do_method).call(vbox)
1137             textview.grab_focus  #- because if moving, focus is stolen
1138             done
1139         }
1140         if perform.call
1141             save_undo(_("move %s") % direction,
1142                       proc {
1143                           autotable.method(undo_method).call(vbox)
1144                           textview.grab_focus  #- because if moving, focus is stolen
1145                           autoscroll_if_needed($autotable_sw, img, textview)
1146                           $notebook.set_page(1)
1147                           proc {
1148                               autotable.method(do_method).call(vbox)
1149                               textview.grab_focus  #- because if moving, focus is stolen
1150                               autoscroll_if_needed($autotable_sw, img, textview)
1151                               $notebook.set_page(1)
1152                           }
1153                       })
1154         end
1155     }
1156
1157     color_swap_and_cleanup = proc {
1158         perform_color_swap_and_cleanup = proc {
1159             color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1160             my_gen_real_thumbnail.call
1161         }
1162
1163         cleanup_all_thumbnails.call
1164         perform_color_swap_and_cleanup.call
1165
1166         save_undo(_("color swap"),
1167                   proc {
1168                       perform_color_swap_and_cleanup.call
1169                       textview.grab_focus
1170                       autoscroll_if_needed($autotable_sw, img, textview)
1171                       $notebook.set_page(1)
1172                       proc {
1173                           perform_color_swap_and_cleanup.call
1174                           textview.grab_focus
1175                           autoscroll_if_needed($autotable_sw, img, textview)
1176                           $notebook.set_page(1)
1177                       }
1178                   })
1179     }
1180
1181     change_frame_offset_and_cleanup_real = proc { |values|
1182         perform_change_frame_offset_and_cleanup = proc { |val|
1183             change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
1184             my_gen_real_thumbnail.call
1185         }
1186         perform_change_frame_offset_and_cleanup.call(values[:new])
1187         
1188         save_undo(_("specify frame offset"),
1189                   proc {
1190                       perform_change_frame_offset_and_cleanup.call(values[:old])
1191                       textview.grab_focus
1192                       autoscroll_if_needed($autotable_sw, img, textview)
1193                       $notebook.set_page(1)
1194                       proc {
1195                           perform_change_frame_offset_and_cleanup.call(values[:new])
1196                           textview.grab_focus
1197                           autoscroll_if_needed($autotable_sw, img, textview)
1198                           $notebook.set_page(1)
1199                       }
1200                   })
1201     }
1202
1203     change_frame_offset_and_cleanup = proc {
1204         if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
1205             change_frame_offset_and_cleanup_real.call(values)
1206         end
1207     }
1208
1209     change_pano_amount_and_cleanup_real = proc { |values|
1210         perform_change_pano_amount_and_cleanup = proc { |val|
1211             change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1212         }
1213         perform_change_pano_amount_and_cleanup.call(values[:new])
1214         
1215         save_undo(_("change panorama amount"),
1216                   proc {
1217                       perform_change_pano_amount_and_cleanup.call(values[:old])
1218                       textview.grab_focus
1219                       autoscroll_if_needed($autotable_sw, img, textview)
1220                       $notebook.set_page(1)
1221                       proc {
1222                           perform_change_pano_amount_and_cleanup.call(values[:new])
1223                           textview.grab_focus
1224                           autoscroll_if_needed($autotable_sw, img, textview)
1225                           $notebook.set_page(1)
1226                       }
1227                   })
1228     }
1229
1230     change_pano_amount_and_cleanup = proc {
1231         if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1232             change_pano_amount_and_cleanup_real.call(values)
1233         end
1234     }
1235
1236     whitebalance_and_cleanup_real = proc { |values|
1237         perform_change_whitebalance_and_cleanup = proc { |val|
1238             change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1239             recalc_whitebalance(val, fullpath, thumbnail_img, img,
1240                                 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1241             cleanup_all_thumbnails.call
1242         }
1243         perform_change_whitebalance_and_cleanup.call(values[:new])
1244
1245         save_undo(_("fix white balance"),
1246                   proc {
1247                       perform_change_whitebalance_and_cleanup.call(values[:old])
1248                       textview.grab_focus
1249                       autoscroll_if_needed($autotable_sw, img, textview)
1250                       $notebook.set_page(1)
1251                       proc {
1252                           perform_change_whitebalance_and_cleanup.call(values[:new])
1253                           textview.grab_focus
1254                           autoscroll_if_needed($autotable_sw, img, textview)
1255                           $notebook.set_page(1)
1256                       }
1257                   })
1258     }
1259
1260     whitebalance_and_cleanup = proc {
1261         if values = ask_whitebalance(fullpath, thumbnail_img, img,
1262                                      $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1263             whitebalance_and_cleanup_real.call(values)
1264         end
1265     }
1266
1267      gammacorrect_and_cleanup_real = proc { |values|
1268          perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1269              change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1270              recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1271                                  $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1272              cleanup_all_thumbnails.call
1273          }
1274          perform_change_gammacorrect_and_cleanup.call(values[:new])
1275  
1276          save_undo(_("gamma correction"),
1277                    Proc.new {
1278                        perform_change_gammacorrect_and_cleanup.call(values[:old])
1279                        textview.grab_focus
1280                        autoscroll_if_needed($autotable_sw, img, textview)
1281                        $notebook.set_page(1)
1282                        Proc.new {
1283                            perform_change_gammacorrect_and_cleanup.call(values[:new])
1284                            textview.grab_focus
1285                            autoscroll_if_needed($autotable_sw, img, textview)
1286                            $notebook.set_page(1)
1287                        }
1288                    })
1289      }
1290  
1291      gammacorrect_and_cleanup = Proc.new {
1292          if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1293                                       $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1294              gammacorrect_and_cleanup_real.call(values)
1295          end
1296      }
1297
1298     enhance_and_cleanup = proc {
1299         perform_enhance_and_cleanup = proc {
1300             enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1301             my_gen_real_thumbnail.call
1302         }
1303
1304         cleanup_all_thumbnails.call
1305         perform_enhance_and_cleanup.call
1306
1307         save_undo(_("enhance"),
1308                   proc {
1309                       perform_enhance_and_cleanup.call
1310                       textview.grab_focus
1311                       autoscroll_if_needed($autotable_sw, img, textview)
1312                       $notebook.set_page(1)
1313                       proc {
1314                           perform_enhance_and_cleanup.call
1315                           textview.grab_focus
1316                           autoscroll_if_needed($autotable_sw, img, textview)
1317                           $notebook.set_page(1)
1318                       }
1319                   })
1320     }
1321
1322     delete = proc { |isacut|
1323         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 })
1324             $modified = true
1325             after = nil
1326             perform_delete = proc {
1327                 after = autotable.get_next_widget(vbox)
1328                 if !after
1329                     after = autotable.get_previous_widget(vbox)
1330                 end
1331                 if $config['deleteondisk'] && !isacut
1332                     msg 3, "scheduling for delete: #{fullpath}"
1333                     $todelete << fullpath
1334                 end
1335                 autotable.remove(vbox)
1336                 if after
1337                     $vbox2widgets[after][:textview].grab_focus
1338                     autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1339                 end
1340             }
1341             
1342             previous_pos = autotable.get_current_number(vbox)
1343             perform_delete.call
1344
1345             if !after
1346                 delete_current_subalbum
1347             else
1348                 save_undo(_("delete"),
1349                           proc { |pos|
1350                               autotable.reinsert(pos, vbox, filename)
1351                               $notebook.set_page(1)
1352                               autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1353                               $cuts = []
1354                               msg 3, "removing deletion schedule of: #{fullpath}"
1355                               $todelete.delete(fullpath)  #- unconditional because deleteondisk option could have been modified
1356                               proc {
1357                                   perform_delete.call
1358                                   $notebook.set_page(1)
1359                               }
1360                           }, previous_pos)
1361             end
1362         end
1363     }
1364
1365     cut = proc {
1366         delete.call(true)
1367         $cuts << { :vbox => vbox, :filename => filename }
1368         $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1369     }
1370     paste = proc {
1371         if $cuts.size > 0
1372             $cuts.each { |elem|
1373                 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1374             }
1375             last = $cuts[-1]
1376             autotable.queue_draws << proc {
1377                 $vbox2widgets[last[:vbox]][:textview].grab_focus
1378                 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1379             }
1380             save_undo(_("paste"),
1381                       proc { |cuts|
1382                           cuts.each { |elem| autotable.remove(elem[:vbox]) }
1383                           $notebook.set_page(1)
1384                           proc {
1385                               cuts.each { |elem|
1386                                   autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1387                               }
1388                               $notebook.set_page(1)
1389                           }
1390                       }, $cuts)
1391             $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1392             $cuts = []
1393         end
1394     }
1395
1396     $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1397                                  :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1398                                  :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1399                                  :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1400
1401     textview.signal_connect('key-press-event') { |w, event|
1402         propagate = true
1403         if event.state != 0
1404             x, y = autotable.get_current_pos(vbox)
1405             control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1406             shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1407             alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1408             if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1409                 if control_pressed
1410                     if widget_up = autotable.get_widget_at_pos(x, y - 1)
1411                         $vbox2widgets[widget_up][:textview].grab_focus
1412                     end
1413                 end
1414                 if shift_pressed
1415                     move.call('up')
1416                 end
1417             end
1418             if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1419                 if control_pressed
1420                     if widget_down = autotable.get_widget_at_pos(x, y + 1)
1421                         $vbox2widgets[widget_down][:textview].grab_focus
1422                     end
1423                 end
1424                 if shift_pressed
1425                     move.call('down')
1426                 end
1427             end
1428             if event.keyval == Gdk::Keyval::GDK_Left
1429                 if x > 0
1430                     if control_pressed
1431                         $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1432                     end
1433                     if shift_pressed
1434                         move.call('left')
1435                     end
1436                 end
1437                 if alt_pressed
1438                     rotate_and_cleanup.call(-90)
1439                 end
1440             end
1441             if event.keyval == Gdk::Keyval::GDK_Right
1442                 next_ = autotable.get_next_widget(vbox)
1443                 if next_ && autotable.get_current_pos(next_)[0] > x
1444                     if control_pressed
1445                         $vbox2widgets[next_][:textview].grab_focus
1446                     end
1447                     if shift_pressed
1448                         move.call('right')
1449                     end
1450                 end
1451                 if alt_pressed
1452                     rotate_and_cleanup.call(90)
1453                 end
1454             end
1455             if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1456                 delete.call(false)
1457             end
1458             if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1459                 view_element(filename, { :delete => delete })
1460                 propagate = false
1461             end
1462             if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1463                 perform_undo
1464             end
1465             if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1466                 perform_redo
1467             end
1468         end
1469         !propagate  #- propagate if needed
1470     }
1471
1472     $ignore_next_release = false
1473     evtbox.signal_connect('button-press-event') { |w, event|
1474         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1475             if event.state & Gdk::Window::BUTTON3_MASK != 0
1476                 #- gesture redo: hold right mouse button then click left mouse button
1477                 $config['nogestures'] or perform_redo
1478                 $ignore_next_release = true
1479             else
1480                 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1481                 if $r90.active?
1482                     rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1483                 elsif $r270.active?
1484                     rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1485                 elsif $enhance.active?
1486                     enhance_and_cleanup.call
1487                 elsif $delete.active?
1488                     delete.call(false)
1489                 else
1490                     textview.grab_focus
1491                     $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1492                 end
1493             end
1494             $button1_pressed_autotable = true
1495         elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1496             if event.state & Gdk::Window::BUTTON1_MASK != 0
1497                 #- gesture undo: hold left mouse button then click right mouse button
1498                 $config['nogestures'] or perform_undo
1499                 $ignore_next_release = true
1500             end
1501         elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1502             view_element(filename, { :delete => delete })
1503         end
1504         false   #- propagate
1505     }
1506
1507     evtbox.signal_connect('button-release-event') { |w, event|
1508         if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1509             if !$ignore_next_release
1510                 x, y = autotable.get_current_pos(vbox)
1511                 next_ = autotable.get_next_widget(vbox)
1512                 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1513                                      { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1514                                        :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1515                                      { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1516                                        :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1517                                        :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1518                                        :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1519             end
1520             $ignore_next_release = false
1521             $gesture_press = nil
1522         end
1523         false   #- propagate
1524     }
1525
1526     #- handle reordering with drag and drop
1527     Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1528     Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1529     vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1530         selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1531     }
1532
1533     vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1534         done = false
1535         #- mouse gesture first (dnd disables button-release-event)
1536         if $gesture_press && $gesture_press[:filename] == filename
1537             if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1538                 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1539                 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1540                 rotate_and_cleanup.call(angle)
1541                 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1542                 done = true
1543             elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1544                 msg 3, "gesture delete: click-drag right button to the bottom"
1545                 delete.call(false)
1546                 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1547                 done = true
1548             end
1549         end
1550         if !done
1551             ctxt.targets.each { |target|
1552                 if target.name == 'reorder-elements'
1553                     move_dnd = proc { |from,to|
1554                         if from != to
1555                             $modified = true
1556                             autotable.move(from, to)
1557                             save_undo(_("reorder"),
1558                                       proc { |from, to|
1559                                           if to > from
1560                                               autotable.move(to - 1, from)
1561                                           else
1562                                               autotable.move(to, from + 1)
1563                                           end
1564                                           $notebook.set_page(1)
1565                                           proc {
1566                                               autotable.move(from, to)
1567                                               $notebook.set_page(1)
1568                                           }
1569                                       }, from, to)
1570                         end
1571                     }
1572                     if $multiple_dnd.size == 0
1573                         move_dnd.call(selection_data.data.to_i,
1574                                       autotable.get_current_number(vbox))
1575                     else
1576                         UndoHandler.begin_batch
1577                         $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1578                                       each { |path|
1579                             #- need to update current position between each call
1580                             move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1581                                           autotable.get_current_number(vbox))
1582                         }
1583                         UndoHandler.end_batch
1584                     end
1585                     $multiple_dnd = []
1586                 end
1587             }
1588         end
1589     }
1590
1591     vbox.show_all
1592 end
1593
1594 def create_auto_table
1595
1596     $autotable = Gtk::AutoTable.new(5)
1597
1598     $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1599     thumbnails_vb = Gtk::VBox.new(false, 5)
1600
1601     frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1602     $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1603     thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1604     thumbnails_vb.add($autotable)
1605
1606     $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1607     $autotable_sw.add_with_viewport(thumbnails_vb)
1608
1609     #- follows stuff for handling multiple elements selection
1610     press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1611     gc = nil
1612     update_selected = proc {
1613         $autotable.current_order.each { |path|
1614             w = $name2widgets[path][:evtbox].window
1615             xm = w.position[0] + w.size[0]/2
1616             ym = w.position[1] + w.size[1]/2
1617             if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1618                 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1619                     $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1620                     $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1621                 end
1622             end
1623             if $selected_elements[path] && ! $selected_elements[path][:keep]
1624                 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))
1625                     $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1626                     $selected_elements.delete(path)
1627                 end
1628             end
1629         }
1630     }
1631     $autotable.signal_connect('realize') { |w,e|
1632         gc = Gdk::GC.new($autotable.window)
1633         gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1634         gc.function = Gdk::GC::INVERT
1635         #- autoscroll handling for DND and multiple selections
1636         Gtk.timeout_add(100) {
1637             if ! $autotable.window.nil?
1638                 w, x, y, mask = $autotable.window.pointer
1639                 if mask & Gdk::Window::BUTTON1_MASK != 0
1640                     if y < $autotable_sw.vadjustment.value
1641                         if pos_x
1642                             $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]])
1643                         end
1644                         if $button1_pressed_autotable || press_x
1645                             scroll_upper($autotable_sw, y)
1646                         end
1647                         if not press_x.nil?
1648                             w, pos_x, pos_y = $autotable.window.pointer
1649                             $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]])
1650                             update_selected.call
1651                         end
1652                     end
1653                     if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1654                         if pos_x
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                         end
1657                         if $button1_pressed_autotable || press_x
1658                             scroll_lower($autotable_sw, y)
1659                         end
1660                         if not press_x.nil?
1661                             w, pos_x, pos_y = $autotable.window.pointer
1662                             $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]])
1663                             update_selected.call
1664                         end
1665                     end
1666                 end
1667             end
1668             ! $autotable.window.nil?
1669         }
1670     }
1671
1672     $autotable.signal_connect('button-press-event') { |w,e|
1673         if e.button == 1
1674             if !$button1_pressed_autotable
1675                 press_x = e.x
1676                 press_y = e.y
1677                 if e.state & Gdk::Window::SHIFT_MASK == 0
1678                     $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1679                     $selected_elements = {}
1680                     $statusbar.push(0, utf8(_("Nothing selected.")))
1681                 else
1682                     $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1683                 end
1684                 set_mousecursor(Gdk::Cursor::TCROSS)
1685             end
1686         end
1687     }
1688     $autotable.signal_connect('button-release-event') { |w,e|
1689         if e.button == 1
1690             if $button1_pressed_autotable
1691                 #- unselect all only now
1692                 $multiple_dnd = $selected_elements.keys
1693                 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1694                 $selected_elements = {}
1695                 $button1_pressed_autotable = false
1696             else
1697                 if pos_x
1698                     $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]])
1699                     if $selected_elements.length > 0
1700                         $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1701                     end
1702                 end
1703                 press_x = press_y = pos_x = pos_y = nil
1704                 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1705             end
1706         end
1707     }
1708     $autotable.signal_connect('motion-notify-event') { |w,e|
1709         if ! press_x.nil?
1710             if pos_x
1711                 $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]])
1712             end
1713             pos_x = e.x
1714             pos_y = e.y
1715             $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]])
1716             update_selected.call
1717         end
1718     }
1719
1720 end
1721
1722 def create_subalbums_page
1723
1724     subalbums_hb = Gtk::HBox.new
1725     $subalbums_vb = Gtk::VBox.new(false, 5)
1726     subalbums_hb.pack_start($subalbums_vb, false, false)
1727     $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1728     $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1729     $subalbums_sw.add_with_viewport(subalbums_hb)
1730 end
1731
1732 def save_current_file
1733     save_changes
1734
1735     if $filename
1736         begin
1737             begin
1738                 ios = File.open($filename, "w")
1739                 $xmldoc.write(ios, 0)
1740                 ios.close
1741             rescue Iconv::IllegalSequence
1742                 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1743                 if ! ios.nil? && ! ios.closed?
1744                     ios.close
1745                 end
1746                 $xmldoc.xml_decl.encoding = 'UTF-8'
1747                 ios = File.open($filename, "w")
1748                 $xmldoc.write(ios, 0)
1749                 ios.close
1750             end
1751             return true
1752         rescue Exception
1753             puts $!
1754             return false
1755         end
1756     end
1757 end
1758
1759 def save_current_file_user
1760     save_tempfilename = $filename
1761     $filename = $orig_filename
1762     if ! save_current_file
1763         show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1764         $filename = save_tempfilename
1765         return
1766     end
1767     $modified = false
1768     $generated_outofline = false
1769     $filename = save_tempfilename
1770
1771     msg 3, "performing actual deletion of: " + $todelete.join(', ')
1772     $todelete.each { |f|
1773         system("rm -f #{f}")
1774     }
1775 end
1776
1777 def mark_document_as_dirty
1778     $xmldoc.elements.each('//dir') { |elem|
1779         elem.delete_attribute('already-generated')
1780     }
1781 end
1782
1783 #- ret: true => ok  false => cancel
1784 def ask_save_modifications(msg1, msg2, *options)
1785     ret = true
1786     options = options.size > 0 ? options[0] : {}
1787     if $modified
1788         if options[:disallow_cancel]
1789             dialog = Gtk::Dialog.new(msg1,
1790                                      $main_window,
1791                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1792                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1793                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1794         else
1795             dialog = Gtk::Dialog.new(msg1,
1796                                      $main_window,
1797                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1798                                      [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1799                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1800                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1801         end
1802         dialog.default_response = Gtk::Dialog::RESPONSE_YES
1803         dialog.vbox.add(Gtk::Label.new(msg2))
1804         dialog.window_position = Gtk::Window::POS_CENTER
1805         dialog.show_all
1806         
1807         dialog.run { |response|
1808             dialog.destroy
1809             if response == Gtk::Dialog::RESPONSE_YES
1810                 if ! save_current_file_user
1811                     return ask_save_modifications(msg1, msg2, options)
1812                 end
1813             else
1814                 #- if we have generated an album but won't save modifications, we must remove 
1815                 #- already-generated markers in original file
1816                 if $generated_outofline
1817                     begin
1818                         $xmldoc = REXML::Document.new File.new($orig_filename)
1819                         mark_document_as_dirty
1820                         ios = File.open($orig_filename, "w")
1821                         $xmldoc.write(ios, 0)
1822                         ios.close
1823                     rescue Exception
1824                         puts "exception: #{$!}"
1825                     end
1826                 end
1827             end
1828             if response == Gtk::Dialog::RESPONSE_CANCEL
1829                 ret = false
1830             end
1831             $todelete = []  #- unconditionally clear the list of images/videos to delete
1832         }
1833     end
1834     return ret
1835 end
1836
1837 def try_quit(*options)
1838     if ask_save_modifications(utf8(_("Save before quitting?")),
1839                               utf8(_("Do you want to save your changes before quitting?")),
1840                               *options)
1841         Gtk.main_quit
1842     end
1843 end
1844
1845 def show_popup(parent, msg, *options)
1846     dialog = Gtk::Dialog.new
1847     if options[0] && options[0][:title]
1848         dialog.title = options[0][:title]
1849     else
1850         dialog.title = utf8(_("Booh message"))
1851     end
1852     lbl = Gtk::Label.new
1853     if options[0] && options[0][:nomarkup]
1854         lbl.text = msg
1855     else
1856         lbl.markup = msg
1857     end
1858     if options[0] && options[0][:centered]
1859         lbl.set_justify(Gtk::Justification::CENTER)
1860     end
1861     if options[0] && options[0][:selectable]
1862         lbl.selectable = true
1863     end
1864     if options[0] && options[0][:topwidget]
1865         dialog.vbox.add(options[0][:topwidget])
1866     end
1867     if options[0] && options[0][:scrolled]
1868         sw = Gtk::ScrolledWindow.new(nil, nil)
1869         sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1870         sw.add_with_viewport(lbl)
1871         dialog.vbox.add(sw)
1872         dialog.set_default_size(500, 600)
1873     else
1874         dialog.vbox.add(lbl)
1875         dialog.set_default_size(200, 120)
1876     end
1877     if options[0] && options[0][:okcancel]
1878         dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1879     end
1880     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1881
1882     if options[0] && options[0][:pos_centered]
1883         dialog.window_position = Gtk::Window::POS_CENTER
1884     else
1885         dialog.window_position = Gtk::Window::POS_MOUSE
1886     end
1887
1888     if options[0] && options[0][:linkurl]
1889         linkbut = Gtk::Button.new('')
1890         linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1891         linkbut.signal_connect('clicked') { open_url(options[0][:linkurl] + '/index.html' ) }
1892         linkbut.relief = Gtk::RELIEF_NONE
1893         linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1894         linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1895         dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1896     end
1897
1898     dialog.show_all
1899
1900     if !options[0] || !options[0][:not_transient]
1901         dialog.transient_for = parent
1902         dialog.run { |response|
1903             dialog.destroy
1904             if options[0] && options[0][:okcancel]
1905                 return response == Gtk::Dialog::RESPONSE_OK
1906             end
1907         }
1908     else
1909         dialog.signal_connect('response') { dialog.destroy }
1910     end
1911 end
1912
1913 def backend_wait_message(parent, msg, infopipe_path, mode)
1914     w = Gtk::Window.new
1915     w.set_transient_for(parent)
1916     w.modal = true
1917
1918     vb = Gtk::VBox.new(false, 5).set_border_width(5)
1919     vb.pack_start(Gtk::Label.new(msg), false, false)
1920
1921     vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1922     vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1923     if mode != 'one dir scan'
1924         vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1925     end
1926     if mode == 'web-album'
1927         vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1928         vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1929     end
1930     vb.pack_start(Gtk::HSeparator.new, false, false)
1931
1932     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1933     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1934     vb.pack_end(bottom, false, false)
1935
1936     infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1937     refresh_thread = Thread.new {
1938         directories_counter = 0
1939         while line = infopipe.gets
1940             if line =~ /^directories: (\d+), sizes: (\d+)/
1941                 directories = $1.to_f + 1
1942                 sizes = $2.to_f
1943             elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1944                 elements = $3.to_f + 1
1945                 if mode == 'web-album'
1946                     elements += sizes
1947                 end
1948                 element_counter = 0
1949                 gtk_thread_protect { pb1_1.fraction = 0 }
1950                 if mode != 'one dir scan'
1951                     newtext = utf8(full_src_dir_to_rel($1, $2))
1952                     newtext = '/' if newtext == ''
1953                     gtk_thread_protect { pb1_2.text = newtext }
1954                     directories_counter += 1
1955                     gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1956                 end
1957             elsif line =~ /^processing element$/
1958                 element_counter += 1
1959                 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1960             elsif line =~ /^processing size$/
1961                 element_counter += 1
1962                 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1963             elsif line =~ /^finished processing sizes$/
1964                 gtk_thread_protect { pb1_1.fraction = 1 }
1965             elsif line =~ /^creating index.html$/
1966                 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1967                 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1968                 directories_counter = 0
1969             elsif line =~ /^index.html: (.+)\|(.+)/
1970                 newtext = utf8(full_src_dir_to_rel($1, $2))
1971                 newtext = '/' if newtext == ''
1972                 gtk_thread_protect { pb2.text = newtext }
1973                 directories_counter += 1
1974                 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1975             elsif line =~ /^die: (.*)$/
1976                 $diemsg = $1
1977             end
1978         end
1979     }
1980
1981     w.add(vb)
1982     w.signal_connect('delete-event') { w.destroy }
1983     w.signal_connect('destroy') {
1984         Thread.kill(refresh_thread)
1985         gtk_thread_flush  #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1986         if infopipe_path
1987             infopipe.close
1988             system("rm -f #{infopipe_path}")
1989         end
1990     }
1991     w.window_position = Gtk::Window::POS_CENTER
1992     w.show_all
1993
1994     return [ b, w ]
1995 end
1996
1997 def call_backend(cmd, waitmsg, mode, params)
1998     pipe = Tempfile.new("boohpipe")
1999     pipe.close!
2000     system("mkfifo #{pipe.path}")
2001     cmd += " --info-pipe #{pipe.path}"
2002     button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2003     pid = nil
2004     Thread.new {
2005         msg 2, cmd
2006         if pid = fork
2007             id, exitstatus = Process.waitpid2(pid)
2008             gtk_thread_protect { w8.destroy }
2009             if exitstatus == 0
2010                 if params[:successmsg]
2011                     gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2012                 end
2013                 if params[:closure_after]
2014                     gtk_thread_protect(&params[:closure_after])
2015                 end
2016             elsif exitstatus == 15
2017                 #- say nothing, user aborted
2018             else
2019                 gtk_thread_protect { show_popup($main_window,
2020                                                 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2021             end
2022         else
2023             exec(cmd)
2024         end
2025     }
2026     button.signal_connect('clicked') {
2027         Process.kill('SIGTERM', pid)
2028     }
2029 end
2030
2031 def save_changes(*forced)
2032     if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2033         return
2034     end
2035
2036     $xmldir.delete_attribute('already-generated')
2037
2038     propagate_children = proc { |xmldir|
2039         if xmldir.attributes['subdirs-caption']
2040             xmldir.delete_attribute('already-generated')
2041         end
2042         xmldir.elements.each('dir') { |element|
2043             propagate_children.call(element)
2044         }
2045     }
2046
2047     if $xmldir.child_byname_notattr('dir', 'deleted')
2048         new_title = $subalbums_title.buffer.text
2049         if new_title != $xmldir.attributes['subdirs-caption']
2050             parent = $xmldir.parent
2051             if parent.name == 'dir'
2052                 parent.delete_attribute('already-generated')
2053             end
2054             propagate_children.call($xmldir)
2055         end
2056         $xmldir.add_attribute('subdirs-caption', new_title)
2057         $xmldir.elements.each('dir') { |element|
2058             if !element.attributes['deleted']
2059                 path = element.attributes['path']
2060                 newtext = $subalbums_edits[path][:editzone].buffer.text
2061                 if element.attributes['subdirs-caption']
2062                     if element.attributes['subdirs-caption'] != newtext
2063                         propagate_children.call(element)
2064                     end
2065                     element.add_attribute('subdirs-caption',     newtext)
2066                     element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2067                 else
2068                     if element.attributes['thumbnails-caption'] != newtext
2069                         element.delete_attribute('already-generated')
2070                     end
2071                     element.add_attribute('thumbnails-caption',     newtext)
2072                     element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2073                 end
2074             end
2075         }
2076     end
2077
2078     if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2079         if $xmldir.attributes['thumbnails-caption']
2080             path = $xmldir.attributes['path']
2081             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2082         end
2083     elsif $xmldir.attributes['thumbnails-caption']
2084         $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2085     end
2086
2087     if $xmldir.attributes['thumbnails-caption']
2088         if edit = $subalbums_edits[$xmldir.attributes['path']]
2089             $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2090         end
2091     end
2092
2093     #- remove and reinsert elements to reflect new ordering
2094     saves = {}
2095     cpt = 0
2096     $xmldir.elements.each { |element|
2097         if element.name == 'image' || element.name == 'video'
2098             saves[element.attributes['filename']] = element.remove
2099             cpt += 1
2100         end
2101     }
2102     $autotable.current_order.each { |path|
2103         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2104         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2105         saves.delete(path)
2106     }
2107     saves.each_key { |path|
2108         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2109         chld.add_attribute('deleted', 'true')
2110     }
2111 end
2112
2113 def sort_by_exif_date
2114     $modified = true
2115     save_changes
2116     current_order = []
2117     $xmldir.elements.each { |element|
2118         if element.name == 'image' || element.name == 'video'
2119             current_order << element.attributes['filename']
2120         end
2121     }
2122
2123     #- look for EXIF dates
2124     w = Gtk::Window.new
2125     w.set_transient_for($main_window)
2126     w.modal = true
2127     vb = Gtk::VBox.new(false, 5).set_border_width(5)
2128     vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2129     vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2130     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2131     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2132     vb.pack_end(bottom, false, false)
2133     w.add(vb)
2134     w.signal_connect('delete-event') { w.destroy }
2135     w.window_position = Gtk::Window::POS_CENTER
2136     w.show_all
2137
2138     aborted = false
2139     b.signal_connect('clicked') { aborted = true }
2140     dates = {}
2141     i = 0
2142     current_order.each { |f|
2143         i += 1
2144         if entry2type(f) == 'image'
2145             pb.text = f
2146             pb.fraction = i.to_f / current_order.size
2147             Gtk.main_iteration while Gtk.events_pending?
2148             date_time = `identify -format "%[EXIF:DateTime]" '#{from_utf8($current_path + "/" + f)}'`.chomp
2149             if $? == 0 && date_time != ''
2150                 dates[f] = date_time
2151             end
2152         end
2153         if aborted
2154             break
2155         end
2156     }
2157     w.destroy
2158     if aborted
2159         return
2160     end
2161
2162     saves = {}
2163     $xmldir.elements.each { |element|
2164         if element.name == 'image' || element.name == 'video'
2165             saves[element.attributes['filename']] = element.remove
2166         end
2167     }
2168
2169     #- find a good fallback for all entries without a date (still next to the item they were next to)
2170     neworder = dates.keys.sort { |a,b| dates[a] <=> dates[b] }
2171     for i in 0 .. current_order.size - 1
2172         if ! neworder.include?(current_order[i])
2173             j = i - 1
2174             while j > 0 && ! neworder.include?(current_order[j])
2175                 j -= 1
2176             end
2177             neworder[(neworder.index(current_order[j]) || -1 ) + 1, 0] = current_order[i]
2178         end
2179     end
2180     neworder.each { |f|
2181         $xmldir.add_element(saves[f].name, saves[f].attributes)
2182     }
2183
2184     #- let the auto-table reflect new ordering
2185     change_dir
2186 end
2187
2188 def remove_all_captions
2189     $modified = true
2190     texts = {}
2191     $autotable.current_order.each { |path|
2192         texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2193         $name2widgets[File.basename(path)][:textview].buffer.text = ''
2194     }
2195     save_undo(_("remove all captions"),
2196               proc { |texts|
2197                   texts.each_key { |key|
2198                       $name2widgets[key][:textview].buffer.text = texts[key]
2199                   }
2200                   $notebook.set_page(1)
2201                   proc {
2202                       texts.each_key { |key|
2203                           $name2widgets[key][:textview].buffer.text = ''
2204                       }
2205                       $notebook.set_page(1)
2206                   }
2207               }, texts)
2208 end
2209
2210 def change_dir
2211     $selected_elements.each_key { |path|
2212         $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2213     }
2214     $autotable.clear
2215     $vbox2widgets = {}
2216     $name2widgets = {}
2217     $name2closures = {}
2218     $selected_elements = {}
2219     $cuts = []
2220     $multiple_dnd = []
2221     UndoHandler.cleanup
2222     $undo_tb.sensitive = $undo_mb.sensitive = false
2223     $redo_tb.sensitive = $redo_mb.sensitive = false
2224
2225     if !$current_path
2226         return
2227     end
2228
2229     $subalbums_vb.children.each { |chld|
2230         $subalbums_vb.remove(chld)
2231     }
2232     $subalbums = Gtk::Table.new(0, 0, true)
2233     current_y_sub_albums = 0
2234
2235     $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2236     $subalbums_edits = {}
2237     subalbums_counter = 0
2238     subalbums_edits_bypos = {}
2239
2240     add_subalbum = proc { |xmldir, counter|
2241         $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2242         subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2243         if xmldir == $xmldir
2244             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2245             captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2246             caption = xmldir.attributes['thumbnails-caption']
2247             infotype = 'thumbnails'
2248         else
2249             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2250             captionfile, caption = find_subalbum_caption_info(xmldir)
2251             infotype = find_subalbum_info_type(xmldir)
2252         end
2253         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2254         hbox = Gtk::HBox.new
2255         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2256         f = Gtk::Frame.new
2257         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2258
2259         img = nil
2260         my_gen_real_thumbnail = proc {
2261             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2262         }
2263
2264         if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2265             f.add(img = Gtk::Image.new)
2266             my_gen_real_thumbnail.call
2267         else
2268             f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2269         end
2270         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2271         $subalbums.attach(hbox,
2272                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2273
2274         frame, textview = create_editzone($subalbums_sw, 0, img)
2275         textview.buffer.text = caption
2276         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2277                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2278
2279         change_image = proc {
2280             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2281                                             nil,
2282                                             Gtk::FileChooser::ACTION_OPEN,
2283                                             nil,
2284                                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2285             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2286             fc.transient_for = $main_window
2287             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))
2288             f.add(preview_img = Gtk::Image.new)
2289             preview.show_all
2290             fc.signal_connect('update-preview') { |w|
2291                 begin
2292                     if fc.preview_filename
2293                         preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2294                         fc.preview_widget_active = true
2295                     end
2296                 rescue Gdk::PixbufError
2297                     fc.preview_widget_active = false
2298                 end
2299             }
2300             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2301                 $modified = true
2302                 old_file = captionfile
2303                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2304                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2305                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2306                 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2307
2308                 new_file = fc.filename
2309                 msg 3, "new captionfile is: #{fc.filename}"
2310                 perform_changefile = proc {
2311                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2312                     $modified_pixbufs.delete(thumbnail_file)
2313                     xmldir.delete_attribute("#{infotype}-rotate")
2314                     xmldir.delete_attribute("#{infotype}-color-swap")
2315                     xmldir.delete_attribute("#{infotype}-enhance")
2316                     xmldir.delete_attribute("#{infotype}-frame-offset")
2317                     my_gen_real_thumbnail.call
2318                 }
2319                 perform_changefile.call
2320
2321                 save_undo(_("change caption file for sub-album"),
2322                           proc {
2323                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2324                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2325                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2326                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2327                               xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2328                               my_gen_real_thumbnail.call
2329                               $notebook.set_page(0)
2330                               proc {
2331                                   perform_changefile.call
2332                                   $notebook.set_page(0)
2333                               }
2334                           })
2335             end
2336             fc.destroy
2337         }
2338
2339         refresh = proc {
2340             system("rm -f '#{thumbnail_file}'")
2341             my_gen_real_thumbnail.call
2342         }
2343
2344         rotate_and_cleanup = proc { |angle|
2345             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2346             system("rm -f '#{thumbnail_file}'")
2347         }
2348
2349         move = proc { |direction|
2350             $modified = true
2351
2352             save_changes('forced')
2353             oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2354             if direction == 'up'
2355                 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2356                 subalbums_edits_bypos[oldpos - 1][:position] += 1
2357             end
2358             if direction == 'down'
2359                 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2360                 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2361             end
2362             if direction == 'top'
2363                 for i in 1 .. oldpos - 1
2364                     subalbums_edits_bypos[i][:position] += 1
2365                 end
2366                 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2367             end
2368             if direction == 'bottom'
2369                 for i in oldpos + 1 .. subalbums_counter
2370                     subalbums_edits_bypos[i][:position] -= 1
2371                 end
2372                 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2373             end
2374
2375             elems = []
2376             $xmldir.elements.each('dir') { |element|
2377                 if (!element.attributes['deleted'])
2378                     elems << [ element.attributes['path'], element.remove ]
2379                 end
2380             }
2381             elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2382                   each { |e| $xmldir.add_element(e[1]) }
2383             #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2384             $xmldir.elements.each('descendant::dir') { |elem|
2385                 elem.delete_attribute('already-generated')
2386             }
2387
2388             sel = $albums_tv.selection.selected_rows
2389             change_dir
2390             populate_subalbums_treeview(false)
2391             $albums_tv.selection.select_path(sel[0])
2392         }
2393
2394         color_swap_and_cleanup = proc {
2395             perform_color_swap_and_cleanup = proc {
2396                 color_swap(xmldir, "#{infotype}-")
2397                 my_gen_real_thumbnail.call
2398             }
2399             perform_color_swap_and_cleanup.call
2400
2401             save_undo(_("color swap"),
2402                       proc {
2403                           perform_color_swap_and_cleanup.call
2404                           $notebook.set_page(0)
2405                           proc {
2406                               perform_color_swap_and_cleanup.call
2407                               $notebook.set_page(0)
2408                           }
2409                       })
2410         }
2411
2412         change_frame_offset_and_cleanup = proc {
2413             if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2414                 perform_change_frame_offset_and_cleanup = proc { |val|
2415                     change_frame_offset(xmldir, "#{infotype}-", val)
2416                     my_gen_real_thumbnail.call
2417                 }
2418                 perform_change_frame_offset_and_cleanup.call(values[:new])
2419
2420                 save_undo(_("specify frame offset"),
2421                           proc {
2422                               perform_change_frame_offset_and_cleanup.call(values[:old])
2423                               $notebook.set_page(0)
2424                               proc {
2425                                   perform_change_frame_offset_and_cleanup.call(values[:new])
2426                                   $notebook.set_page(0)
2427                               }
2428                           })
2429             end
2430         }
2431
2432         whitebalance_and_cleanup = proc {
2433             if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2434                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2435                 perform_change_whitebalance_and_cleanup = proc { |val|
2436                     change_whitebalance(xmldir, "#{infotype}-", val)
2437                     recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2438                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2439                     system("rm -f '#{thumbnail_file}'")
2440                 }
2441                 perform_change_whitebalance_and_cleanup.call(values[:new])
2442                 
2443                 save_undo(_("fix white balance"),
2444                           proc {
2445                               perform_change_whitebalance_and_cleanup.call(values[:old])
2446                               $notebook.set_page(0)
2447                               proc {
2448                                   perform_change_whitebalance_and_cleanup.call(values[:new])
2449                                   $notebook.set_page(0)
2450                               }
2451                           })
2452             end
2453         }
2454
2455         gammacorrect_and_cleanup = proc {
2456             if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2457                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2458                 perform_change_gammacorrect_and_cleanup = proc { |val|
2459                     change_gammacorrect(xmldir, "#{infotype}-", val)
2460                     recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2461                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2462                     system("rm -f '#{thumbnail_file}'")
2463                 }
2464                 perform_change_gammacorrect_and_cleanup.call(values[:new])
2465                 
2466                 save_undo(_("gamma correction"),
2467                           proc {
2468                               perform_change_gammacorrect_and_cleanup.call(values[:old])
2469                               $notebook.set_page(0)
2470                               proc {
2471                                   perform_change_gammacorrect_and_cleanup.call(values[:new])
2472                                   $notebook.set_page(0)
2473                               }
2474                           })
2475             end
2476         }
2477
2478         enhance_and_cleanup = proc {
2479             perform_enhance_and_cleanup = proc {
2480                 enhance(xmldir, "#{infotype}-")
2481                 my_gen_real_thumbnail.call
2482             }
2483             
2484             perform_enhance_and_cleanup.call
2485             
2486             save_undo(_("enhance"),
2487                       proc {
2488                           perform_enhance_and_cleanup.call
2489                           $notebook.set_page(0)
2490                           proc {
2491                               perform_enhance_and_cleanup.call
2492                               $notebook.set_page(0)
2493                           }
2494                       })
2495         }
2496
2497         evtbox.signal_connect('button-press-event') { |w, event|
2498             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2499                 if $r90.active?
2500                     rotate_and_cleanup.call(90)
2501                 elsif $r270.active?
2502                     rotate_and_cleanup.call(-90)
2503                 elsif $enhance.active?
2504                     enhance_and_cleanup.call
2505                 end
2506             end
2507             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2508                 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2509                                      { :forbid_left => true, :forbid_right => true,
2510                                        :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2511                                        :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2512                                      { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2513                                        :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2514                                        :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2515             end
2516             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2517                 change_image.call
2518                 true   #- handled
2519             end
2520         }
2521         evtbox.signal_connect('button-press-event') { |w, event|
2522             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2523             false
2524         }
2525
2526         evtbox.signal_connect('button-release-event') { |w, event|
2527             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2528                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2529                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2530                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2531                     msg 3, "gesture rotate: #{angle}"
2532                     rotate_and_cleanup.call(angle)
2533                 end
2534             end
2535             $gesture_press = nil
2536         }
2537                 
2538         $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2539         $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2540         current_y_sub_albums += 1
2541     }
2542
2543     if $xmldir.child_byname_notattr('dir', 'deleted')
2544         #- title edition
2545         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2546         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2547         $subalbums_title.set_justification(Gtk::Justification::CENTER)
2548         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2549         #- this album image/caption
2550         if $xmldir.attributes['thumbnails-caption']
2551             add_subalbum.call($xmldir, 0)
2552         end
2553     end
2554     total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2555     $xmldir.elements.each { |element|
2556         if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2557             #- element (image or video) of this album
2558             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2559             msg 3, "dest_img: #{dest_img}"
2560             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2561             total[element.name] += 1
2562         end
2563         if element.name == 'dir' && !element.attributes['deleted']
2564             #- sub-album image/caption
2565             add_subalbum.call(element, subalbums_counter += 1)
2566             total[element.name] += 1
2567         end
2568     }
2569     $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2570                                                                                 total['image'], total['video'], total['dir'] ]))
2571     $subalbums_vb.add($subalbums)
2572     $subalbums_vb.show_all
2573
2574     if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2575         $notebook.get_tab_label($autotable_sw).sensitive = false
2576         $notebook.set_page(0)
2577         $thumbnails_title.buffer.text = ''
2578     else
2579         $notebook.get_tab_label($autotable_sw).sensitive = true
2580         $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2581     end
2582
2583     if !$xmldir.child_byname_notattr('dir', 'deleted')
2584         $notebook.get_tab_label($subalbums_sw).sensitive = false
2585         $notebook.set_page(1)
2586     else
2587         $notebook.get_tab_label($subalbums_sw).sensitive = true
2588     end
2589 end
2590
2591 def pixbuf_or_nil(filename)
2592     begin
2593         return Gdk::Pixbuf.new(filename)
2594     rescue
2595         return nil
2596     end
2597 end
2598
2599 def theme_choose(current)
2600     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2601                              $main_window,
2602                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2603                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2604                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2605
2606     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2607     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2608     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2609     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2610     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2611     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2612     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2613     treeview.signal_connect('button-press-event') { |w, event|
2614         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2615             dialog.response(Gtk::Dialog::RESPONSE_OK)
2616         end
2617     }
2618
2619     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2620
2621     `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2622         dir.chomp!
2623         iter = model.append
2624         iter[0] = File.basename(dir)
2625         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2626         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2627         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2628         if File.basename(dir) == current
2629             treeview.selection.select_iter(iter)
2630         end
2631     }
2632
2633     dialog.set_default_size(700, 400)
2634     dialog.vbox.show_all
2635     dialog.run { |response|
2636         iter = treeview.selection.selected
2637         dialog.destroy
2638         if response == Gtk::Dialog::RESPONSE_OK && iter
2639             return model.get_value(iter, 0)
2640         end
2641     }
2642     return nil
2643 end
2644
2645 def show_password_protections
2646     examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2647         child_iter = $albums_iters[xmldir.attributes['path']]
2648         if xmldir.attributes['password-protect']
2649             child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2650             already_protected = true
2651         elsif already_protected
2652             pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2653             if pix
2654                 pix = pix.saturate_and_pixelate(1, true)
2655             end
2656             child_iter[2] = pix
2657         else
2658             child_iter[2] = nil
2659         end
2660         xmldir.elements.each('dir') { |elem|
2661             if !elem.attributes['deleted']
2662                 examine_dir_elem.call(child_iter, elem, already_protected)
2663             end
2664         }
2665     }
2666     examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2667 end
2668
2669 def populate_subalbums_treeview(select_first)
2670     $albums_ts.clear
2671     $autotable.clear
2672     $albums_iters = {}
2673     $subalbums_vb.children.each { |chld|
2674         $subalbums_vb.remove(chld)
2675     }
2676
2677     source = $xmldoc.root.attributes['source']
2678     msg 3, "source: #{source}"
2679
2680     xmldir = $xmldoc.elements['//dir']
2681     if !xmldir || xmldir.attributes['path'] != source
2682         msg 1, _("Corrupted booh file...")
2683         return
2684     end
2685
2686     append_dir_elem = proc { |parent_iter, xmldir|
2687         child_iter = $albums_ts.append(parent_iter)
2688         child_iter[0] = File.basename(xmldir.attributes['path'])
2689         child_iter[1] = xmldir.attributes['path']
2690         $albums_iters[xmldir.attributes['path']] = child_iter
2691         msg 3, "puttin location: #{xmldir.attributes['path']}"
2692         xmldir.elements.each('dir') { |elem|
2693             if !elem.attributes['deleted']
2694                 append_dir_elem.call(child_iter, elem)
2695             end
2696         }
2697     }
2698     append_dir_elem.call(nil, xmldir)
2699     show_password_protections
2700
2701     $albums_tv.expand_all
2702     if select_first
2703         $albums_tv.selection.select_iter($albums_ts.iter_first)
2704     end
2705 end
2706
2707 def select_current_theme
2708     select_theme($xmldoc.root.attributes['theme'],
2709                  $xmldoc.root.attributes['limit-sizes'],
2710                  !$xmldoc.root.attributes['optimize-for-32'].nil?,
2711                  $xmldoc.root.attributes['thumbnails-per-row'])
2712 end
2713
2714 def open_file(filename)
2715
2716     $filename = nil
2717     $modified = false
2718     $current_path = nil   #- invalidate
2719     $modified_pixbufs = {}
2720     $albums_ts.clear
2721     $autotable.clear
2722     $subalbums_vb.children.each { |chld|
2723         $subalbums_vb.remove(chld)
2724     }
2725
2726     if !File.exists?(filename)
2727         return utf8(_("File not found."))
2728     end
2729
2730     begin
2731         $xmldoc = REXML::Document.new File.new(filename)
2732     rescue Exception
2733         $xmldoc = nil
2734     end
2735
2736     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2737         if entry2type(filename).nil?
2738             return utf8(_("Not a booh file!"))
2739         else
2740             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."))
2741         end
2742     end
2743
2744     if !source = $xmldoc.root.attributes['source']
2745         return utf8(_("Corrupted booh file..."))
2746     end
2747
2748     if !dest = $xmldoc.root.attributes['destination']
2749         return utf8(_("Corrupted booh file..."))
2750     end
2751
2752     if !theme = $xmldoc.root.attributes['theme']
2753         return utf8(_("Corrupted booh file..."))
2754     end
2755
2756     if $xmldoc.root.attributes['version'] < '0.8.6'
2757         msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2758         mark_document_as_dirty
2759         if $xmldoc.root.attributes['version'] < '0.8.4'
2760             msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2761             `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2762                 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2763                 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2764                 if old_dest_dir != new_dest_dir
2765                     sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2766                 end
2767                 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2768                     xmldir.elements.each { |element|
2769                         if %w(image video).include?(element.name) && !element.attributes['deleted']
2770                             old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2771                             new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2772                             Dir[old_name + '*'].each { |file|
2773                                 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2774                                 file != new_file and sys("mv '#{file}' '#{new_file}'")
2775                             }
2776                         end
2777                         if element.name == 'dir' && !element.attributes['deleted']
2778                             old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2779                             new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2780                             old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2781                         end
2782                     }
2783                 else
2784                     msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2785                 end
2786             }
2787         end
2788         $xmldoc.root.add_attribute('version', $VERSION)
2789     end
2790
2791     select_current_theme
2792
2793     $filename = filename
2794     $default_size['thumbnails'] =~ /(.*)x(.*)/
2795     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2796     $albums_thumbnail_size =~ /(.*)x(.*)/
2797     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2798
2799     populate_subalbums_treeview(true)
2800
2801     $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
2802     return nil
2803 end
2804
2805 def open_file_user(filename)
2806     result = open_file(filename)
2807     if !result
2808         $config['last-opens'] ||= []
2809         if $config['last-opens'][-1] != utf8(filename)
2810             $config['last-opens'] << utf8(filename)
2811         end
2812         $orig_filename = $filename
2813         tmp = Tempfile.new("boohtemp")
2814         tmp.close!
2815         #- for security
2816         ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2817         ios.close
2818         $tempfiles << $filename << "#{$filename}.backup"
2819     else
2820         $orig_filename = nil
2821     end
2822     return result
2823 end
2824
2825 def open_file_popup
2826     if !ask_save_modifications(utf8(_("Save this album?")),
2827                                utf8(_("Do you want to save the changes to this album?")),
2828                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2829         return
2830     end
2831     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2832                                     nil,
2833                                     Gtk::FileChooser::ACTION_OPEN,
2834                                     nil,
2835                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2836     fc.add_shortcut_folder(File.expand_path("~/.booh"))
2837     fc.set_current_folder(File.expand_path("~/.booh"))
2838     fc.transient_for = $main_window
2839     ok = false
2840     while !ok
2841         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2842             push_mousecursor_wait(fc)
2843             msg = open_file_user(fc.filename)
2844             pop_mousecursor(fc)
2845             if msg
2846                 show_popup(fc, msg)
2847                 ok = false
2848             else
2849                 ok = true
2850             end
2851         else
2852             ok = true
2853         end
2854     end
2855     fc.destroy
2856 end
2857
2858 def open_url(url)
2859     cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2860     msg 2, cmd
2861     system(cmd)
2862 end
2863
2864 def additional_booh_options
2865     options = ''
2866     if $config['mproc']
2867         options += "--mproc #{$config['mproc'].to_i} "
2868     end
2869     options += "--comments-format '#{$config['comments-format']}'"
2870     return options
2871 end
2872
2873 def new_album
2874     if !ask_save_modifications(utf8(_("Save this album?")),
2875                                utf8(_("Do you want to save the changes to this album?")),
2876                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2877         return
2878     end
2879     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2880                              $main_window,
2881                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2882                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2883                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2884     
2885     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2886     tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2887                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2888     tbl.attach(src = Gtk::Entry.new,
2889                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2890     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2891                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2892     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2893                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2894     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2895                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2896     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2897                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2898     tbl.attach(dest = Gtk::Entry.new,
2899                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2900     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2901                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2902     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2903                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2904     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2905                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2906     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2907                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2908
2909     tooltips = Gtk::Tooltips.new
2910     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2911     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2912                          pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2913     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2914                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
2915     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2916     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)
2917     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2918                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2919     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
2920                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
2921     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)
2922     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2923                                    pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
2924     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)
2925
2926     src_nb_calculated_for = ''
2927     src_nb_thread = nil
2928     process_src_nb = proc {
2929         if src.text != src_nb_calculated_for
2930             src_nb_calculated_for = src.text
2931             if src_nb_thread
2932                 Thread.kill(src_nb_thread)
2933                 src_nb_thread = nil
2934             end
2935             if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
2936                 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
2937             else
2938                 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2939                     if File.readable?(from_utf8_safe(src_nb_calculated_for))
2940                         src_nb_thread = Thread.new {
2941                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2942                             total = { 'image' => 0, 'video' => 0, nil => 0 }
2943                             `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2944                                 if File.basename(dir) =~ /^\./
2945                                     next
2946                                 else
2947                                     begin
2948                                         Dir.entries(dir.chomp).each { |file|
2949                                             total[entry2type(file)] += 1
2950                                         }
2951                                     rescue Errno::EACCES, Errno::ENOENT
2952                                     end
2953                                 end
2954                             }
2955                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2956                             src_nb_thread = nil
2957                         }
2958                     else
2959                         src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2960                     end
2961                 else
2962                     src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2963                 end
2964             end
2965         end
2966         true
2967     }
2968     timeout_src_nb = Gtk.timeout_add(100) {
2969         process_src_nb.call
2970     }
2971
2972     src_browse.signal_connect('clicked') {
2973         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2974                                         nil,
2975                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
2976                                         nil,
2977                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2978         fc.transient_for = $main_window
2979         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2980             src.text = utf8(fc.filename)
2981             process_src_nb.call
2982             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2983         end
2984         fc.destroy
2985     }
2986
2987     dest_browse.signal_connect('clicked') {
2988         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2989                                         nil,
2990                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
2991                                         nil,
2992                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2993         fc.transient_for = $main_window
2994         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2995             dest.text = utf8(fc.filename)
2996         end
2997         fc.destroy
2998     }
2999
3000     conf_browse.signal_connect('clicked') {
3001         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3002                                         nil,
3003                                         Gtk::FileChooser::ACTION_SAVE,
3004                                         nil,
3005                                         [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3006         fc.transient_for = $main_window
3007         fc.add_shortcut_folder(File.expand_path("~/.booh"))
3008         fc.set_current_folder(File.expand_path("~/.booh"))
3009         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3010             conf.text = utf8(fc.filename)
3011         end
3012         fc.destroy
3013     }
3014
3015     theme_sizes = []
3016     nperrows = []
3017     recreate_theme_config = proc {
3018         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3019         theme_sizes = []
3020         select_theme(theme_button.label, 'all', optimize432.active?, nil)
3021         $images_size.each { |s|
3022             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3023             if !s['optional']
3024                 cb.active = true
3025             end
3026             tooltips.set_tip(cb, utf8(s['description']), nil)
3027             theme_sizes << { :widget => cb, :value => s['name'] }
3028         }
3029         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3030         tooltips = Gtk::Tooltips.new
3031         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3032         theme_sizes << { :widget => cb, :value => 'original' }
3033         sizes.show_all
3034
3035         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3036         nperrow_group = nil
3037         nperrows = []
3038         $allowed_N_values.each { |n|
3039             if nperrow_group
3040                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3041             else
3042                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3043             end
3044             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3045             if $default_N == n
3046                 rb.active = true
3047             end
3048             nperrows << { :widget => rb, :value => n }
3049         }
3050         nperrowradios.show_all
3051     }
3052     recreate_theme_config.call
3053
3054     theme_button.signal_connect('clicked') {
3055         if newtheme = theme_choose(theme_button.label)
3056             theme_button.label = newtheme
3057             recreate_theme_config.call
3058         end
3059     }
3060
3061     dialog.vbox.add(frame1)
3062     dialog.vbox.add(frame2)
3063     dialog.window_position = Gtk::Window::POS_MOUSE
3064     dialog.show_all
3065
3066     keepon = true
3067     ok = true
3068     while keepon
3069         dialog.run { |response|
3070             if response == Gtk::Dialog::RESPONSE_OK
3071                 srcdir = from_utf8_safe(src.text)
3072                 destdir = from_utf8_safe(dest.text)
3073                 confpath = from_utf8_safe(conf.text)
3074                 if src.text != '' && srcdir == ''
3075                     show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3076                     src.grab_focus
3077                 elsif !File.directory?(srcdir)
3078                     show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3079                     src.grab_focus
3080                 elsif dest.text != '' && destdir == ''
3081                     show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3082                     dest.grab_focus
3083                 elsif destdir != make_dest_filename(destdir)
3084                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3085                     dest.grab_focus
3086                 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3087                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
3088                     dest.grab_focus
3089                 elsif File.exists?(destdir) && !File.directory?(destdir)
3090                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3091                     dest.grab_focus
3092                 elsif conf.text == ''
3093                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3094                     conf.grab_focus
3095                 elsif conf.text != '' && confpath == ''
3096                     show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3097                     conf.grab_focus
3098                 elsif File.directory?(confpath)
3099                     show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3100                     conf.grab_focus
3101                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3102                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3103                 else
3104                     system("mkdir '#{destdir}'")
3105                     if !File.directory?(destdir)
3106                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3107                         dest.grab_focus
3108                     else
3109                         keepon = false
3110                     end
3111                 end
3112             else
3113                 keepon = ok = false
3114             end
3115         }
3116     end
3117     if ok
3118         srcdir = from_utf8(src.text)
3119         destdir = from_utf8(dest.text)
3120         configskel = File.expand_path(from_utf8(conf.text))
3121         theme = theme_button.label
3122         sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3123         nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3124         opt432 = optimize432.active?
3125         madewith = madewithentry.text
3126         indexlink = indexlinkentry.text
3127     end
3128     if src_nb_thread
3129         Thread.kill(src_nb_thread)
3130         gtk_thread_flush  #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3131     end
3132     dialog.destroy
3133     Gtk.timeout_remove(timeout_src_nb)
3134
3135     if ok
3136         call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3137                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3138                      "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3139                      utf8(_("Please wait while scanning source directory...")),
3140                      'full scan',
3141                      { :closure_after => proc { open_file_user(configskel) } })
3142     end
3143 end
3144
3145 def properties
3146     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3147                              $main_window,
3148                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3149                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3150                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3151     
3152     source = $xmldoc.root.attributes['source']
3153     dest = $xmldoc.root.attributes['destination']
3154     theme = $xmldoc.root.attributes['theme']
3155     opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3156     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3157     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3158     if limit_sizes
3159         limit_sizes = limit_sizes.split(/,/)
3160     end
3161     madewith = $xmldoc.root.attributes['made-with']
3162     indexlink = $xmldoc.root.attributes['index-link']
3163
3164     tooltips = Gtk::Tooltips.new
3165     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3166     tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3167                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3168     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3169                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3170     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3171                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3172     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3173                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3174     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3175                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3176     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3177                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3178
3179     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3180     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3181                                    pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3182     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3183                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3184     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3185     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)
3186     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3187                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3188
3189     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3190                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3191     if indexlink
3192         indexlinkentry.text = indexlink
3193     end
3194     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)
3195     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3196                                    pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3197     if madewith
3198         madewithentry.text = madewith
3199     end
3200     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)
3201
3202     theme_sizes = []
3203     nperrows = []
3204     recreate_theme_config = proc {
3205         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3206         theme_sizes = []
3207         select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3208
3209         $images_size.each { |s|
3210             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3211             if limit_sizes
3212                 if limit_sizes.include?(s['name'])
3213                     cb.active = true
3214                 end
3215             else
3216                 if !s['optional']
3217                     cb.active = true
3218                 end
3219             end
3220             tooltips.set_tip(cb, utf8(s['description']), nil)
3221             theme_sizes << { :widget => cb, :value => s['name'] }
3222         }
3223         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3224         tooltips = Gtk::Tooltips.new
3225         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3226         if limit_sizes && limit_sizes.include?('original')
3227             cb.active = true
3228         end
3229         theme_sizes << { :widget => cb, :value => 'original' }
3230         sizes.show_all
3231
3232         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3233         nperrow_group = nil
3234         nperrows = []
3235         $allowed_N_values.each { |n|
3236             if nperrow_group
3237                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3238             else
3239                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3240             end
3241             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3242             nperrowradios.add(Gtk::Label.new('  '))
3243             if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3244                 rb.active = true
3245             end
3246             nperrows << { :widget => rb, :value => n.to_s }
3247         }
3248         nperrowradios.show_all
3249     }
3250     recreate_theme_config.call
3251
3252     theme_button.signal_connect('clicked') {
3253         if newtheme = theme_choose(theme_button.label)
3254             limit_sizes = nil
3255             nperrow = nil
3256             theme_button.label = newtheme
3257             recreate_theme_config.call
3258         end
3259     }
3260
3261     dialog.vbox.add(frame1)
3262     dialog.vbox.add(frame2)
3263     dialog.window_position = Gtk::Window::POS_MOUSE
3264     dialog.show_all
3265
3266     keepon = true
3267     ok = true
3268     while keepon
3269         dialog.run { |response|
3270             if response == Gtk::Dialog::RESPONSE_OK
3271                 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3272                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3273                 else
3274                     keepon = false
3275                 end
3276             else
3277                 keepon = ok = false
3278             end
3279         }
3280     end
3281     save_theme = theme_button.label
3282     save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3283     save_opt432 = optimize432.active?
3284     save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3285     save_madewith = madewithentry.text
3286     save_indexlink = indexlinkentry.text
3287     dialog.destroy
3288
3289     if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_madewith != madewith || save_indexlink != indexlinkentry)
3290         mark_document_as_dirty
3291         save_current_file
3292         call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3293                      "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3294                      "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3295                      utf8(_("Please wait while scanning source directory...")),
3296                      'full scan',
3297                      { :closure_after => proc {
3298                              open_file($filename)
3299                              $modified = true
3300                          } })
3301     else
3302         #- select_theme merges global variables, need to return to current choices
3303         select_current_theme
3304     end
3305 end
3306
3307 def merge_current
3308     save_current_file
3309
3310     sel = $albums_tv.selection.selected_rows
3311
3312     call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3313                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
3314                  utf8(_("Please wait while scanning source directory...")),
3315                  'one dir scan',
3316                  { :closure_after => proc {
3317                          open_file($filename)
3318                          $albums_tv.selection.select_path(sel[0])
3319                          $modified = true
3320                      } })
3321 end
3322
3323 def merge_newsubs
3324     save_current_file
3325
3326     sel = $albums_tv.selection.selected_rows
3327
3328     call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3329                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
3330                  utf8(_("Please wait while scanning source directory...")),
3331                  'subdirs scan',
3332                  { :closure_after => proc {
3333                          open_file($filename)
3334                          $albums_tv.selection.select_path(sel[0])
3335                          $modified = true
3336                      } })
3337 end
3338
3339 def merge
3340     save_current_file
3341
3342     theme = $xmldoc.root.attributes['theme']
3343     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3344     if limit_sizes
3345         limit_sizes = "--sizes #{limit_sizes}"
3346     end
3347     call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3348                  "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3349                  utf8(_("Please wait while scanning source directory...")),
3350                  'full scan',
3351                  { :closure_after => proc {
3352                          open_file($filename)
3353                          $modified = true
3354                      } })
3355 end
3356
3357 def save_as_do
3358     fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3359                                     nil,
3360                                     Gtk::FileChooser::ACTION_SAVE,
3361                                     nil,
3362                                     [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3363     fc.transient_for = $main_window
3364     fc.add_shortcut_folder(File.expand_path("~/.booh"))
3365     fc.set_current_folder(File.expand_path("~/.booh"))
3366     fc.filename = $orig_filename
3367     if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3368         $orig_filename = fc.filename
3369         if ! save_current_file_user
3370             fc.destroy
3371             return save_as_do
3372         end
3373         $config['last-opens'] ||= []
3374         $config['last-opens'] << $orig_filename
3375     end
3376     fc.destroy
3377 end
3378
3379 def preferences
3380     dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3381                              $main_window,
3382                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3383                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3384                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3385
3386     dialog.vbox.add(notebook = Gtk::Notebook.new)
3387     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3388     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3389                0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3390     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)),
3391                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3392     tooltips = Gtk::Tooltips.new
3393     tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3394 for example: /usr/bin/mplayer %f")), nil)
3395     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3396                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3397     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3398                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3399     tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3400 for example: /usr/bin/gimp-remote %f")), nil)
3401     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3402                0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3403     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3404                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3405     tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3406 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3407     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3408                0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3409     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)),
3410                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3411     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)
3412     tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3413                0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3414     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)
3415     tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3416                0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3417     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)
3418
3419     smp_check.signal_connect('toggled') {
3420         if smp_check.active?
3421             smp_hbox.sensitive = true
3422         else
3423             smp_hbox.sensitive = false
3424         end
3425     }
3426     if $config['mproc']
3427         smp_check.active = true
3428         smp_spin.value = $config['mproc'].to_i
3429     end
3430     nogestures_check.active = $config['nogestures']
3431     deleteondisk_check.active = $config['deleteondisk']
3432
3433     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3434     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3435                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3436     tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3437                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3438     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3439                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3440     tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3441                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3442     tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3443                2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3444     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)
3445     commentsformat_help.signal_connect('clicked') {
3446         show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3447 hence you should look at ImageMagick/identify documentation for the most    
3448 accurate and up-to-date documentation. Last time I checked, documentation
3449 was:
3450
3451 Print information about the image in a format of your choosing. You can
3452 include the image filename, type, width, height, Exif data, or other image
3453 attributes by embedding special format characters:                          
3454
3455                      %O   page offset
3456                      %P   page width and height                             
3457                      %b   file size                                         
3458                      %c   comment                                           
3459                      %d   directory                                         
3460                      %e   filename extension                                
3461                      %f   filename                                          
3462                      %g   page geometry                                     
3463                      %h   height                                            
3464                      %i   input filename                                    
3465                      %k   number of unique colors                           
3466                      %l   label                                             
3467                      %m   magick                                            
3468                      %n   number of scenes                                  
3469                      %o   output filename                                   
3470                      %p   page number                                       
3471                      %q   quantum depth                                     
3472                      %r   image class and colorspace                        
3473                      %s   scene number                                      
3474                      %t   top of filename                                   
3475                      %u   unique temporary filename                         
3476                      %w   width                                             
3477                      %x   x resolution                                      
3478                      %y   y resolution                                      
3479                      %z   image depth                       &