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