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