26be6d96cb3a774c7ffd63bd97abe3f77e32682d
[booh] / bin / booh
1 #! /usr/bin/ruby
2 #
3 #                         *  BOOH  *
4 #
5 # A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
6 #
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
11 #
12 #
13 # Copyright (c) 2004-2006 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
14 #
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21
22 require 'getoptlong'
23 require 'tempfile'
24 require 'thread'
25
26 require 'gtk2'
27 require 'booh/libadds'
28 require 'booh/GtkAutoTable'
29
30 require 'gettext'
31 include GetText
32 bindtextdomain("booh")
33
34 require 'rexml/document'
35 include REXML
36
37 require 'booh/booh-lib'
38 include Booh
39 require 'booh/UndoHandler'
40
41
42 #- options
43 $options = [
44     [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       _("Get help message") ],
45
46     [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
47 ]
48
49 #- default values for some globals 
50 $xmldir = nil
51 $modified = false
52 $current_cursor = nil
53 $ignore_videos = false
54 $button1_pressed_autotable = false
55 $generated_outofline = false
56
57 def usage
58     puts _("Usage: %s [OPTION]...") % File.basename($0)
59     $options.each { |ary|
60         printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
61     }
62 end
63
64 def handle_options
65     parser = GetoptLong.new
66     parser.set_options(*$options.collect { |ary| ary[0..2] })
67     begin
68         parser.each_option do |name, arg|
69             case name
70             when '--help'
71                 usage
72                 exit(0)
73
74             when '--verbose-level'
75                 $verbose_level = arg.to_i
76
77             end
78         end
79     rescue
80         puts $!
81         usage
82         exit(1)
83     end
84 end
85
86 def read_config
87     $config = {}
88     $config_file = File.expand_path('~/.booh-gui-rc')
89     if File.readable?($config_file)
90         $xmldoc = REXML::Document.new(File.new($config_file))
91         $xmldoc.root.elements.each { |element|
92             txt = element.get_text
93             if txt
94                 if txt.value =~ /~~~/ || element.name == 'last-opens'
95                     $config[element.name] = txt.value.split(/~~~/)
96                 else
97                     $config[element.name] = txt.value
98                 end
99             elsif element.elements.size == 0
100                 $config[element.name] = ''
101             else
102                 $config[element.name] = {}
103                 element.each { |chld|
104                     txt = chld.get_text
105                     $config[element.name][chld.name] = txt ? txt.value : nil
106                 }
107             end
108         }
109     end
110     $config['video-viewer'] ||= '/usr/bin/mplayer %f'
111     $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
112     $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
113     $config['comments-format'] ||= '%t'
114     if !FileTest.directory?(File.expand_path('~/.booh'))
115         system("mkdir ~/.booh")
116     end
117
118     $tempfiles = []
119     $todelete = []
120 end
121
122 def check_config
123     if !system("which convert >/dev/null 2>/dev/null")
124         show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
125 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
126         exit 1
127     end
128     if !system("which identify >/dev/null 2>/dev/null")
129         show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
130 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
131     end
132     missing = %w(transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
133     if missing != []
134         show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
135     end
136
137     viewer_binary = $config['video-viewer'].split.first
138     if viewer_binary && !File.executable?(viewer_binary)
139         show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
140 You should fix this in Edit/Preferences so that you can view videos.
141
142 Problem was: '%s' is not an executable file.
143 Hint: don't forget to specify the full path to the executable,
144 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
145     end
146     image_editor_binary = $config['image-editor'].split.first
147     if image_editor_binary && !File.executable?(image_editor_binary)
148         show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
149 You should fix this in Edit/Preferences so that you can edit images externally.
150
151 Problem was: '%s' is not an executable file.
152 Hint: don't forget to specify the full path to the executable,
153 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
154     end
155     browser_binary = $config['browser'].split.first
156     if browser_binary && !File.executable?(browser_binary)
157         show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
158 You should fix this in Edit/Preferences so that you can open URLs.
159
160 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
161     end
162 end
163
164 def write_config
165     if $config['last-opens'] && $config['last-opens'].size > 10
166         $config['last-opens'] = $config['last-opens'][-10, 10]
167     end
168
169     ios = File.open($config_file, "w")
170     $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
171     $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
172     $config.each_pair { |key, value|
173         elem = $xmldoc.root.add_element key
174         if value.is_a? Hash
175             $config[key].each_pair { |subkey, subvalue|
176                 subelem = elem.add_element subkey
177                 subelem.add_text subvalue.to_s
178             }
179         elsif value.is_a? Array
180             elem.add_text value.join('~~~')
181         else
182             if !value
183                 elem.remove
184             else
185                 elem.add_text value.to_s
186             end
187         end
188     }
189     $xmldoc.write(ios, 0)
190     ios.close
191
192     $tempfiles.each { |f|
193         system("rm -f #{f}")
194     }
195 end
196
197 def set_mousecursor(what, *widget)
198     cursor = what.nil? ? nil : Gdk::Cursor.new(what)
199     if widget[0] && widget[0].window
200         widget[0].window.cursor = cursor
201     end
202     if $main_window && $main_window.window
203         $main_window.window.cursor = cursor
204     end
205     $current_cursor = what
206 end
207 def set_mousecursor_wait(*widget)
208     gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
209     if Thread.current == Thread.main
210         Gtk.main_iteration while Gtk.events_pending?
211     end
212 end
213 def set_mousecursor_normal(*widget)
214     gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
215 end
216 def push_mousecursor_wait(*widget)
217     if $current_cursor != Gdk::Cursor::WATCH
218         $save_cursor = $current_cursor
219         gtk_thread_protect { set_mousecursor_wait(*widget) }
220     end
221 end
222 def pop_mousecursor(*widget)
223     gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
224 end
225
226 def current_dest_dir
227     source = $xmldoc.root.attributes['source']
228     dest = $xmldoc.root.attributes['destination']
229     return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
230 end
231
232 def full_src_dir_to_rel(path, source)
233     return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
234 end
235
236 def build_full_dest_filename(filename)
237     return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
238 end
239
240 def save_undo(name, closure, *params)
241     UndoHandler.save_undo(name, closure, [ *params ])
242     $undo_tb.sensitive = $undo_mb.sensitive = true
243     $redo_tb.sensitive = $redo_mb.sensitive = false
244 end
245
246 def view_element(filename, closures)
247     if entry2type(filename) == 'video'
248         cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
249         msg 2, cmd
250         system(cmd)
251         return
252     end
253
254     w = Gtk::Window.new.set_title(filename)
255
256     msg 3, "filename: #{filename}"
257     dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
258     #- typically this file won't exist in case of videos; try with the largest thumbnail around
259     if !File.exists?(dest_img)
260         if entry2type(filename) == 'video'
261             alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
262             if not alternatives.empty?
263                 dest_img = alternatives[-1]
264             end
265         else
266             push_mousecursor_wait
267             gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
268             pop_mousecursor
269             if !File.exists?(dest_img)
270                 msg 2, _("Could not generate fullscreen thumbnail!")
271                 return
272                 end
273         end
274     end
275     evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(Gtk::Image.new(dest_img)).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
276     evt.signal_connect('button-press-event') { |this, event|
277         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
278             $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
279         end
280         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
281             menu = Gtk::Menu.new
282             menu.append(delete_item  = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
283             delete_item.signal_connect('activate') {
284                 w.destroy
285                 closures[:delete].call(false)
286             }
287             menu.show_all
288             menu.popup(nil, nil, event.button, event.time)
289         end
290     }
291     evt.signal_connect('button-release-event') { |this, event|
292         if $gesture_press
293             if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
294                 msg 3, "gesture delete: click-drag right button to the bottom"
295                 w.destroy
296                 closures[:delete].call(false)
297                 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
298             end
299         end
300     }
301     tooltips = Gtk::Tooltips.new
302     tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
303
304     w.signal_connect('key-press-event') { |w,event|
305         if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
306             w.destroy
307             closures[:delete].call(false)
308         end
309     }
310
311     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
312     b.signal_connect('clicked') { w.destroy }
313
314     vb = Gtk::VBox.new
315     vb.pack_start(evt, false, false)
316     vb.pack_end(bottom, false, false)
317
318     w.add(vb)
319     w.signal_connect('delete-event') { w.destroy }
320     w.window_position = Gtk::Window::POS_CENTER
321     w.show_all
322 end
323
324 def scroll_upper(scrolledwindow, ypos_top)
325     newval = scrolledwindow.vadjustment.value -
326         ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
327     if newval < scrolledwindow.vadjustment.lower
328         newval = scrolledwindow.vadjustment.lower
329     end
330     scrolledwindow.vadjustment.value = newval
331 end
332
333 def scroll_lower(scrolledwindow, ypos_bottom)
334     newval = scrolledwindow.vadjustment.value +
335         ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
336     if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
337         newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
338     end
339     scrolledwindow.vadjustment.value = newval
340 end
341
342 def autoscroll_if_needed(scrolledwindow, image, textview)
343     #- autoscroll if cursor or image is not visible, if possible
344     if image && image.window || textview.window
345         ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
346         ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
347         current_miny_visible = scrolledwindow.vadjustment.value
348         current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
349         if ypos_top < current_miny_visible
350             scroll_upper(scrolledwindow, ypos_top)
351         elsif ypos_bottom > current_maxy_visible
352             scroll_lower(scrolledwindow, ypos_bottom)
353         end
354     end
355 end
356
357 def create_editzone(scrolledwindow, pagenum, image)
358     frame = Gtk::Frame.new
359     frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
360     frame.set_shadow_type(Gtk::SHADOW_IN)
361     textview.signal_connect('key-press-event') { |w, event|
362         textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
363         if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
364             scrolledwindow.signal_emit('key-press-event', event)
365         end
366         if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
367            event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
368             if event.keyval == Gdk::Keyval::GDK_Up
369                 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
370                     scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
371                 else
372                     scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
373                 end
374             else
375                 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
376                     scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
377                 else
378                     scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
379                 end
380             end
381         end
382         false  #- propagate
383     }
384
385     candidate_undo_text = nil
386     textview.signal_connect('focus-in-event') { |w, event|
387         textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
388         candidate_undo_text = textview.buffer.text
389         false  #- propagate
390     }
391
392     textview.signal_connect('key-release-event') { |w, event|
393         if candidate_undo_text && candidate_undo_text != textview.buffer.text
394             $modified = true
395             save_undo(_("text edit"),
396                       proc { |text|
397                           save_text = textview.buffer.text
398                           textview.buffer.text = text
399                           textview.grab_focus
400                           $notebook.set_page(pagenum)
401                           proc {
402                               textview.buffer.text = save_text
403                               textview.grab_focus
404                               $notebook.set_page(pagenum)
405                           }
406                       }, candidate_undo_text)
407             candidate_undo_text = nil
408         end
409
410         if event.state != 0 || ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
411             autoscroll_if_needed(scrolledwindow, image, textview)
412         end
413         false  #- propagate
414     }
415
416     return [ frame, textview ]
417 end
418
419 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
420
421     if !$modified_pixbufs[thumbnail_img]
422         $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
423     elsif !$modified_pixbufs[thumbnail_img][:orig]
424         $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
425     end
426
427     pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
428
429     #- rotate
430     if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
431         pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
432         msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
433         if pixbuf.height > desired_y
434             pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
435         elsif pixbuf.width < desired_x && pixbuf.height < desired_y
436             pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
437         end
438     end
439
440     #- fix white balance
441     if $modified_pixbufs[thumbnail_img][:whitebalance]
442         pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
443     end
444
445     #- fix gamma correction
446     if $modified_pixbufs[thumbnail_img][:gammacorrect]
447         pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
448     end
449
450     img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
451 end
452
453 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
454     $modified = true
455
456     #- update rotate attribute
457     xmlelem.add_attribute("#{attributes_prefix}rotate", ((xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360).to_s)
458
459     $modified_pixbufs[thumbnail_img] ||= {}
460     $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
461     msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
462
463     update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
464 end
465
466 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
467     $modified = true
468
469     rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
470
471     save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
472               proc { |angle|
473                   rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
474                   $notebook.set_page(attributes_prefix != '' ? 0 : 1)
475                   proc {
476                       rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
477                       $notebook.set_page(0)
478                       $notebook.set_page(attributes_prefix != '' ? 0 : 1)
479                   }
480               }, -angle)
481 end
482
483 def color_swap(xmldir, attributes_prefix)
484     $modified = true
485     if xmldir.attributes["#{attributes_prefix}color-swap"]
486         xmldir.delete_attribute("#{attributes_prefix}color-swap")
487     else
488         xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
489     end
490 end
491
492 def enhance(xmldir, attributes_prefix)
493     $modified = true
494     if xmldir.attributes["#{attributes_prefix}enhance"]
495         xmldir.delete_attribute("#{attributes_prefix}enhance")
496     else
497         xmldir.add_attribute("#{attributes_prefix}enhance", '1')
498     end
499 end
500
501 def change_frame_offset(xmldir, attributes_prefix, value)
502     $modified = true
503     xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
504 end
505
506 def ask_new_frame_offset(xmldir, attributes_prefix)
507     if xmldir
508         value = xmldir.attributes["#{attributes_prefix}frame-offset"]
509     else
510         value = ''
511     end
512
513     dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
514                              $main_window,
515                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
516                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
517                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
518
519     lbl = Gtk::Label.new
520     lbl.markup = utf8(
521 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
522 from. There are approximately 25 frames per second in a video.
523 "))
524     dialog.vbox.add(lbl)
525     dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
526     entry.signal_connect('key-press-event') { |w, event|
527         if event.keyval == Gdk::Keyval::GDK_Return
528             dialog.response(Gtk::Dialog::RESPONSE_OK)
529             true
530         elsif event.keyval == Gdk::Keyval::GDK_Escape
531             dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
532             true
533         else
534             false  #- propagate if needed
535         end
536     }
537
538     dialog.window_position = Gtk::Window::POS_MOUSE
539     dialog.show_all
540
541     dialog.run { |response|
542         newval = entry.text
543         dialog.destroy
544         if response == Gtk::Dialog::RESPONSE_OK
545             $modified = true
546             msg 3, "changing frame offset to #{newval}"
547             return { :old => value, :new => newval }
548         else
549             return nil
550         end
551     }
552 end
553
554 def change_pano_amount(xmldir, attributes_prefix, value)
555     $modified = true
556     if value.nil?
557         xmldir.delete_attribute("#{attributes_prefix}pano-amount")
558     else
559         xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
560     end
561 end
562
563 def ask_new_pano_amount(xmldir, attributes_prefix)
564     if xmldir
565         value = xmldir.attributes["#{attributes_prefix}pano-amount"]
566     else
567         value = nil
568     end
569
570     dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
571                              $main_window,
572                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
573                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
574                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
575
576     lbl = Gtk::Label.new
577     lbl.markup = utf8(
578 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
579 of this panorama image compared to other regular images. For example, if the panorama
580 was taken out of four photos on one row, counting the necessary overlap, the width of
581 this panorama image should probably be roughly three times the width of regular images.
582
583 With this information, booh will be able to generate panorama thumbnails looking
584 the right 'size'.
585 "))
586     dialog.vbox.add(lbl)
587     dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("none (not a panorama image)")))).
588                                                                          add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
589                                                                          add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
590                                                                          add(Gtk::Label.new(utf8(_("times the width of other images"))))))
591     spin.signal_connect('value-changed') {
592         rb_yes.active = true
593     }
594     dialog.window_position = Gtk::Window::POS_MOUSE
595     dialog.show_all
596     if value
597         spin.value = value.to_f
598         rb_yes.active = true
599         spin.grab_focus
600     else
601         rb_no.active = true
602     end
603
604     dialog.run { |response|
605         if rb_no.active?
606             newval = nil
607         else
608             newval = spin.value.to_f
609         end
610         dialog.destroy
611         if response == Gtk::Dialog::RESPONSE_OK
612             $modified = true
613             msg 3, "changing panorama amount to #{newval}"
614             return { :old => value, :new => newval }
615         else
616             return nil
617         end
618     }
619 end
620
621 def change_whitebalance(xmlelem, attributes_prefix, value)
622     $modified = true
623     xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
624 end
625
626 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
627
628     #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
629     if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
630         save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
631         xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
632         save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
633         xmlelem.delete_attribute("#{attributes_prefix}white-balance")
634         destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
635         gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
636                                 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
637         $modified_pixbufs[thumbnail_img] ||= {}
638         $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
639         xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
640         if save_gammacorrect
641             xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
642             $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
643         end
644         $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
645     end
646
647     $modified_pixbufs[thumbnail_img] ||= {}
648     $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
649
650     update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
651 end
652
653 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
654     #- init $modified_pixbufs correctly
655 #    update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
656
657     value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
658
659     dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
660                              $main_window,
661                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
662                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
663                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
664
665     lbl = Gtk::Label.new
666     lbl.markup = utf8(
667 _("You can fix the <b>white balance</b> of the image, if your image is too blue
668 or too yellow because your camera didn't detect the light correctly. Drag the
669 slider below the image to the left for more blue, to the right for more yellow.
670 "))
671     dialog.vbox.add(lbl)
672     if img_
673         dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
674     end
675     dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
676     
677     dialog.window_position = Gtk::Window::POS_MOUSE
678     dialog.show_all
679
680     lastval = nil
681     timeout = Gtk.timeout_add(100) {
682         if hs.value != lastval
683             lastval = hs.value
684             if img_
685                 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
686             end
687         end
688         true
689     }
690
691     dialog.run { |response|
692         Gtk.timeout_remove(timeout)
693         if response == Gtk::Dialog::RESPONSE_OK
694             $modified = true
695             newval = hs.value.to_s
696             msg 3, "changing white balance to #{newval}"
697             dialog.destroy
698             return { :old => value, :new => newval }
699         else
700             if thumbnail_img
701                 $modified_pixbufs[thumbnail_img] ||= {}
702                 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
703                 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
704             end
705             dialog.destroy
706             return nil
707         end
708     }
709 end
710
711 def change_gammacorrect(xmlelem, attributes_prefix, value)
712     $modified = true
713     xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
714 end
715
716 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
717
718     #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
719     if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
720         save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
721         xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
722         save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
723         xmlelem.delete_attribute("#{attributes_prefix}white-balance")
724         destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
725         gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
726                                 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
727         $modified_pixbufs[thumbnail_img] ||= {}
728         $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
729         xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
730         if save_whitebalance
731             xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
732             $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
733         end
734         $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
735     end
736
737     $modified_pixbufs[thumbnail_img] ||= {}
738     $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
739
740     update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
741 end
742
743 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
744     #- init $modified_pixbufs correctly
745 #    update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
746
747     value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
748
749     dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
750                              $main_window,
751                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
752                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
753                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
754
755     lbl = Gtk::Label.new
756     lbl.markup = utf8(
757 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
758 or too bright. Drag the slider below the image.
759 "))
760     dialog.vbox.add(lbl)
761     if img_
762         dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
763     end
764     dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
765     
766     dialog.window_position = Gtk::Window::POS_MOUSE
767     dialog.show_all
768
769     lastval = nil
770     timeout = Gtk.timeout_add(100) {
771         if hs.value != lastval
772             lastval = hs.value
773             if img_
774                 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
775             end
776         end
777         true
778     }
779
780     dialog.run { |response|
781         Gtk.timeout_remove(timeout)
782         if response == Gtk::Dialog::RESPONSE_OK
783             $modified = true
784             newval = hs.value.to_s
785             msg 3, "gamma correction to #{newval}"
786             dialog.destroy
787             return { :old => value, :new => newval }
788         else
789             if thumbnail_img
790                 $modified_pixbufs[thumbnail_img] ||= {}
791                 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
792                 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
793             end
794             dialog.destroy
795             return nil
796         end
797     }
798 end
799
800 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
801     system("rm -f '#{destfile}'")
802     #- type can be 'element' or 'subdir'
803     if type == 'element'
804         gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
805     else
806         gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
807     end
808 end
809
810 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
811     Thread.new {
812         push_mousecursor_wait
813         gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
814         gtk_thread_protect {
815             img.set(destfile)
816             $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
817         }
818         pop_mousecursor
819     }
820 end
821
822 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
823     distribute_multiple_call = Proc.new { |action, arg|
824         $selected_elements.each_key { |path|
825             $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
826         }
827         if possible_actions[:can_multiple] && $selected_elements.length > 0
828             UndoHandler.begin_batch
829             $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
830             UndoHandler.end_batch
831         else
832             closures[action].call(arg)
833         end
834         $selected_elements = {}
835     }
836     menu = Gtk::Menu.new
837     if optionals.include?('change_image')
838         menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
839         changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
840         changeimg.signal_connect('activate') { closures[:change].call }
841         menu.append(Gtk::SeparatorMenuItem.new)
842     end
843     if !possible_actions[:can_multiple] || $selected_elements.length == 0
844         if closures[:view]
845             if type == 'image'
846                 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
847                 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
848                 view.signal_connect('activate') { closures[:view].call }
849             else
850                 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
851                 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
852                 view.signal_connect('activate') { closures[:view].call }
853                 menu.append(Gtk::SeparatorMenuItem.new)
854             end
855         end
856         if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
857             menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
858             exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
859             exif.signal_connect('activate') { show_popup($main_window,
860                                                          utf8(`identify -format "%[EXIF:*]" #{fullpath}`.sub(/MakerNote.*\n/, '')),
861                                                          { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
862             menu.append(Gtk::SeparatorMenuItem.new)
863         end
864     end
865     menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
866     r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
867     r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
868     menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
869     r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
870     r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
871     if !possible_actions[:can_multiple] || $selected_elements.length == 0
872         menu.append(Gtk::SeparatorMenuItem.new)
873         if !possible_actions[:forbid_left]
874             menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
875             moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
876             moveleft.signal_connect('activate') { closures[:move].call('left') }
877             if !possible_actions[:can_left]
878                 moveleft.sensitive = false
879             end
880         end
881         if !possible_actions[:forbid_right]
882             menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
883             moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
884             moveright.signal_connect('activate') { closures[:move].call('right') }
885             if !possible_actions[:can_right]
886                 moveright.sensitive = false
887             end
888         end
889         if optionals.include?('move_top')
890             menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
891             movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
892             movetop.signal_connect('activate') { closures[:move].call('top') }
893             if !possible_actions[:can_top]
894                 movetop.sensitive = false
895             end
896         end
897         menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
898         moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
899         moveup.signal_connect('activate') { closures[:move].call('up') }
900         if !possible_actions[:can_up]
901             moveup.sensitive = false
902         end
903         menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
904         movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
905         movedown.signal_connect('activate') { closures[:move].call('down') }
906         if !possible_actions[:can_down]
907             movedown.sensitive = false
908         end
909         if optionals.include?('move_bottom')
910             menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
911             movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
912             movebottom.signal_connect('activate') { closures[:move].call('bottom') }
913             if !possible_actions[:can_bottom]
914                 movebottom.sensitive = false
915             end
916         end
917     end
918     if type == 'video'
919         if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
920             menu.append(Gtk::SeparatorMenuItem.new)
921             menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
922             color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
923             color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
924             menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
925             flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
926             flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
927             menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
928             frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
929             frame_offset.signal_connect('activate') {
930                 if possible_actions[:can_multiple] && $selected_elements.length > 0
931                     if values = ask_new_frame_offset(nil, '')
932                         distribute_multiple_call.call(:frame_offset, values)
933                     end
934                 else
935                     closures[:frame_offset].call
936                 end
937             }
938         end
939     end
940     menu.append(               Gtk::SeparatorMenuItem.new)
941     menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
942     gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
943     gammacorrect.signal_connect('activate') { 
944         if possible_actions[:can_multiple] && $selected_elements.length > 0
945             if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
946                 distribute_multiple_call.call(:gammacorrect, values)
947             end
948         else
949             closures[:gammacorrect].call
950         end
951     }
952     menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
953     whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
954     whitebalance.signal_connect('activate') { 
955         if possible_actions[:can_multiple] && $selected_elements.length > 0
956             if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
957                 distribute_multiple_call.call(:whitebalance, values)
958             end
959         else
960             closures[:whitebalance].call
961         end
962     }
963     if !possible_actions[:can_multiple] || $selected_elements.length == 0
964         menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
965                                                                                                              _("Enhance constrast"))))
966     else
967         menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
968     end
969     enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
970     enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
971     if type == 'image' && possible_actions[:can_panorama]
972         menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
973         panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
974         panorama.signal_connect('activate') {
975             if possible_actions[:can_multiple] && $selected_elements.length > 0
976                 if values = ask_new_pano_amount(nil, '')
977                     distribute_multiple_call.call(:pano, values)
978                 end
979             else
980                 distribute_multiple_call.call(:pano)
981             end
982        }
983     end
984     menu.append(               Gtk::SeparatorMenuItem.new)
985     if optionals.include?('delete')
986         menu.append(cut_item     = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
987         cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
988         if !possible_actions[:can_multiple] || $selected_elements.length == 0
989             menu.append(paste_item   = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
990             paste_item.signal_connect('activate') { closures[:paste].call }
991             menu.append(clear_item   = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
992             clear_item.signal_connect('activate') { $cuts = [] }
993             if $cuts.size == 0
994                 paste_item.sensitive = clear_item.sensitive = false
995             end
996         end
997         menu.append(               Gtk::SeparatorMenuItem.new)
998     end
999     if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1000         menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1001         editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1002         editexternally.signal_connect('activate') {
1003             cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1004             msg 2, cmd
1005             system(cmd)
1006         }
1007     end
1008     menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1009     refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1010     if optionals.include?('delete')
1011         menu.append(delete_item  = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1012         delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1013     end
1014     menu.show_all
1015     menu.popup(nil, nil, event.button, event.time)
1016 end
1017
1018 def delete_current_subalbum
1019     $modified = true
1020     sel = $albums_tv.selection.selected_rows
1021     $xmldir.elements.each { |e|
1022         if e.name == 'image' || e.name == 'video'
1023             e.add_attribute('deleted', 'true')
1024         end
1025     }
1026     #- branch if we have a non deleted subalbum
1027     if $xmldir.child_byname_notattr('dir', 'deleted')
1028         $xmldir.delete_attribute('thumbnails-caption')
1029         $xmldir.delete_attribute('thumbnails-captionfile')
1030     else
1031         $xmldir.add_attribute('deleted', 'true')
1032         moveup = $xmldir
1033         while moveup.parent.name == 'dir'
1034             moveup = moveup.parent
1035             if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1036                 moveup.add_attribute('deleted', 'true')
1037             else
1038                 break
1039             end
1040         end
1041         sel[0].up!
1042     end
1043     save_changes('forced')
1044     populate_subalbums_treeview(false)
1045     $albums_tv.selection.select_path(sel[0])
1046 end
1047
1048 def restore_deleted
1049     $modified = true
1050     save_changes
1051     $current_path = nil  #- prevent save_changes from being rerun again
1052     sel = $albums_tv.selection.selected_rows
1053     restore_one = proc { |xmldir|
1054         xmldir.elements.each { |e|
1055             if e.name == 'dir' && e.attributes['deleted']
1056                 restore_one.call(e)
1057             end
1058             e.delete_attribute('deleted')
1059         }
1060     }
1061     restore_one.call($xmldir)
1062     populate_subalbums_treeview(false)
1063     $albums_tv.selection.select_path(sel[0])
1064 end
1065
1066 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1067
1068     img = nil
1069     frame1 = Gtk::Frame.new
1070     fullpath = from_utf8("#{$current_path}/#{filename}")
1071
1072     my_gen_real_thumbnail = proc {
1073         gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1074     }
1075
1076     if type == 'video'
1077         pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1078         frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1079                                  pack_start(img = Gtk::Image.new).
1080                                  pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1081         px, mask = pxb.render_pixmap_and_mask(0)
1082         da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1083         da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1084     else
1085         frame1.add(img = Gtk::Image.new)
1086     end
1087
1088     #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1089     if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1090         my_gen_real_thumbnail.call
1091     else
1092         img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1093     end
1094
1095     evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1096
1097     tooltips = Gtk::Tooltips.new
1098     tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1099     tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1100
1101     frame2, textview = create_editzone($autotable_sw, 1, img)
1102     textview.buffer.text = caption
1103     textview.set_justification(Gtk::Justification::CENTER)
1104
1105     vbox = Gtk::VBox.new(false, 5)
1106     vbox.pack_start(evtbox, false, false)
1107     vbox.pack_start(frame2, false, false)
1108     autotable.append(vbox, filename)
1109
1110     #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1111     $vbox2widgets[vbox] = { :textview => textview, :image => img }
1112
1113     #- to be able to find widgets by name
1114     $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1115
1116     cleanup_all_thumbnails = proc {
1117         #- remove out of sync images
1118         dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1119         for sizeobj in $images_size
1120             system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
1121         end
1122
1123     }
1124
1125     refresh = proc {
1126         cleanup_all_thumbnails.call
1127         my_gen_real_thumbnail.call
1128     }
1129  
1130     rotate_and_cleanup = proc { |angle|
1131         cleanup_all_thumbnails.call
1132         rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1133     }
1134
1135     move = proc { |direction|
1136         do_method = "move_#{direction}"
1137         undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1138         perform = proc {
1139             done = autotable.method(do_method).call(vbox)
1140             textview.grab_focus  #- because if moving, focus is stolen
1141             done
1142         }
1143         if perform.call
1144             save_undo(_("move %s") % direction,
1145                       proc {
1146                           autotable.method(undo_method).call(vbox)
1147                           textview.grab_focus  #- because if moving, focus is stolen
1148                           autoscroll_if_needed($autotable_sw, img, textview)
1149                           $notebook.set_page(1)
1150                           proc {
1151                               autotable.method(do_method).call(vbox)
1152                               textview.grab_focus  #- because if moving, focus is stolen
1153                               autoscroll_if_needed($autotable_sw, img, textview)
1154                               $notebook.set_page(1)
1155                           }
1156                       })
1157         end
1158     }
1159
1160     color_swap_and_cleanup = proc {
1161         perform_color_swap_and_cleanup = proc {
1162             cleanup_all_thumbnails.call
1163             color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1164             my_gen_real_thumbnail.call
1165         }
1166
1167         perform_color_swap_and_cleanup.call
1168
1169         save_undo(_("color swap"),
1170                   proc {
1171                       perform_color_swap_and_cleanup.call
1172                       textview.grab_focus
1173                       autoscroll_if_needed($autotable_sw, img, textview)
1174                       $notebook.set_page(1)
1175                       proc {
1176                           perform_color_swap_and_cleanup.call
1177                           textview.grab_focus
1178                           autoscroll_if_needed($autotable_sw, img, textview)
1179                           $notebook.set_page(1)
1180                       }
1181                   })
1182     }
1183
1184     change_frame_offset_and_cleanup_real = proc { |values|
1185         perform_change_frame_offset_and_cleanup = proc { |val|
1186             cleanup_all_thumbnails.call
1187             change_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '', val)
1188             my_gen_real_thumbnail.call
1189         }
1190         perform_change_frame_offset_and_cleanup.call(values[:new])
1191         
1192         save_undo(_("specify frame offset"),
1193                   proc {
1194                       perform_change_frame_offset_and_cleanup.call(values[:old])
1195                       textview.grab_focus
1196                       autoscroll_if_needed($autotable_sw, img, textview)
1197                       $notebook.set_page(1)
1198                       proc {
1199                           perform_change_frame_offset_and_cleanup.call(values[:new])
1200                           textview.grab_focus
1201                           autoscroll_if_needed($autotable_sw, img, textview)
1202                           $notebook.set_page(1)
1203                       }
1204                   })
1205     }
1206
1207     change_frame_offset_and_cleanup = proc {
1208         if values = ask_new_frame_offset($xmldir.elements["*[@filename='#{filename}']"], '')
1209             change_frame_offset_and_cleanup_real.call(values)
1210         end
1211     }
1212
1213     change_pano_amount_and_cleanup_real = proc { |values|
1214         perform_change_pano_amount_and_cleanup = proc { |val|
1215             cleanup_all_thumbnails.call
1216             change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1217         }
1218         perform_change_pano_amount_and_cleanup.call(values[:new])
1219         
1220         save_undo(_("change panorama amount"),
1221                   proc {
1222                       perform_change_pano_amount_and_cleanup.call(values[:old])
1223                       textview.grab_focus
1224                       autoscroll_if_needed($autotable_sw, img, textview)
1225                       $notebook.set_page(1)
1226                       proc {
1227                           perform_change_pano_amount_and_cleanup.call(values[:new])
1228                           textview.grab_focus
1229                           autoscroll_if_needed($autotable_sw, img, textview)
1230                           $notebook.set_page(1)
1231                       }
1232                   })
1233     }
1234
1235     change_pano_amount_and_cleanup = proc {
1236         if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1237             change_pano_amount_and_cleanup_real.call(values)
1238         end
1239     }
1240
1241     whitebalance_and_cleanup_real = proc { |values|
1242         perform_change_whitebalance_and_cleanup = proc { |val|
1243             cleanup_all_thumbnails.call
1244             change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1245             recalc_whitebalance(val, fullpath, thumbnail_img, img,
1246                                 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1247         }
1248         perform_change_whitebalance_and_cleanup.call(values[:new])
1249
1250         save_undo(_("fix white balance"),
1251                   proc {
1252                       perform_change_whitebalance_and_cleanup.call(values[:old])
1253                       textview.grab_focus
1254                       autoscroll_if_needed($autotable_sw, img, textview)
1255                       $notebook.set_page(1)
1256                       proc {
1257                           perform_change_whitebalance_and_cleanup.call(values[:new])
1258                           textview.grab_focus
1259                           autoscroll_if_needed($autotable_sw, img, textview)
1260                           $notebook.set_page(1)
1261                       }
1262                   })
1263     }
1264
1265     whitebalance_and_cleanup = proc {
1266         if values = ask_whitebalance(fullpath, thumbnail_img, img,
1267                                      $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1268             whitebalance_and_cleanup_real.call(values)
1269         end
1270     }
1271
1272      gammacorrect_and_cleanup_real = proc { |values|
1273          perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1274              cleanup_all_thumbnails.call
1275              change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1276              recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1277                                  $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1278          }
1279          perform_change_gammacorrect_and_cleanup.call(values[:new])
1280  
1281          save_undo(_("gamma correction"),
1282                    Proc.new {
1283                        perform_change_gammacorrect_and_cleanup.call(values[:old])
1284                        textview.grab_focus
1285                        autoscroll_if_needed($autotable_sw, img, textview)
1286                        $notebook.set_page(1)
1287                        Proc.new {
1288                            perform_change_gammacorrect_and_cleanup.call(values[:new])
1289                            textview.grab_focus
1290                            autoscroll_if_needed($autotable_sw, img, textview)
1291                            $notebook.set_page(1)
1292                        }
1293                    })
1294      }
1295  
1296      gammacorrect_and_cleanup = Proc.new {
1297          if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1298                                       $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1299              gammacorrect_and_cleanup_real.call(values)
1300          end
1301      }
1302
1303     enhance_and_cleanup = proc {
1304         perform_enhance_and_cleanup = proc {
1305             cleanup_all_thumbnails.call
1306             enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1307             my_gen_real_thumbnail.call
1308         }
1309
1310         cleanup_all_thumbnails.call
1311         perform_enhance_and_cleanup.call
1312
1313         save_undo(_("enhance"),
1314                   proc {
1315                       perform_enhance_and_cleanup.call
1316                       textview.grab_focus
1317                       autoscroll_if_needed($autotable_sw, img, textview)
1318                       $notebook.set_page(1)
1319                       proc {
1320                           perform_enhance_and_cleanup.call
1321                           textview.grab_focus
1322                           autoscroll_if_needed($autotable_sw, img, textview)
1323                           $notebook.set_page(1)
1324                       }
1325                   })
1326     }
1327
1328     delete = proc { |isacut|
1329         if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed? This operation cannot be undone.")), { :okcancel => true })
1330             $modified = true
1331             after = nil
1332             perform_delete = proc {
1333                 after = autotable.get_next_widget(vbox)
1334                 if !after
1335                     after = autotable.get_previous_widget(vbox)
1336                 end
1337                 if $config['deleteondisk'] && !isacut
1338                     msg 3, "scheduling for delete: #{fullpath}"
1339                     $todelete << fullpath
1340                 end
1341                 autotable.remove(vbox)
1342                 if after
1343                     $vbox2widgets[after][:textview].grab_focus
1344                     autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1345                 end
1346             }
1347             
1348             previous_pos = autotable.get_current_number(vbox)
1349             perform_delete.call
1350
1351             if !after
1352                 delete_current_subalbum
1353             else
1354                 save_undo(_("delete"),
1355                           proc { |pos|
1356                               autotable.reinsert(pos, vbox, filename)
1357                               $notebook.set_page(1)
1358                               autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1359                               $cuts = []
1360                               msg 3, "removing deletion schedule of: #{fullpath}"
1361                               $todelete.delete(fullpath)  #- unconditional because deleteondisk option could have been modified
1362                               proc {
1363                                   perform_delete.call
1364                                   $notebook.set_page(1)
1365                               }
1366                           }, previous_pos)
1367             end
1368         end
1369     }
1370
1371     cut = proc {
1372         delete.call(true)
1373         $cuts << { :vbox => vbox, :filename => filename }
1374         $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1375     }
1376     paste = proc {
1377         if $cuts.size > 0
1378             $cuts.each { |elem|
1379                 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1380             }
1381             last = $cuts[-1]
1382             autotable.queue_draws << proc {
1383                 $vbox2widgets[last[:vbox]][:textview].grab_focus
1384                 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1385             }
1386             save_undo(_("paste"),
1387                       proc { |cuts|
1388                           cuts.each { |elem| autotable.remove(elem[:vbox]) }
1389                           $notebook.set_page(1)
1390                           proc {
1391                               cuts.each { |elem|
1392                                   autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1393                               }
1394                               $notebook.set_page(1)
1395                           }
1396                       }, $cuts)
1397             $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1398             $cuts = []
1399         end
1400     }
1401
1402     $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1403                                  :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
1404                                  :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1405                                  :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1406
1407     textview.signal_connect('key-press-event') { |w, event|
1408         propagate = true
1409         if event.state != 0
1410             x, y = autotable.get_current_pos(vbox)
1411             control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1412             shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1413             alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1414             if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1415                 if control_pressed
1416                     if widget_up = autotable.get_widget_at_pos(x, y - 1)
1417                         $vbox2widgets[widget_up][:textview].grab_focus
1418                     end
1419                 end
1420                 if shift_pressed
1421                     move.call('up')
1422                 end
1423             end
1424             if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1425                 if control_pressed
1426                     if widget_down = autotable.get_widget_at_pos(x, y + 1)
1427                         $vbox2widgets[widget_down][:textview].grab_focus
1428                     end
1429                 end
1430                 if shift_pressed
1431                     move.call('down')
1432                 end
1433             end
1434             if event.keyval == Gdk::Keyval::GDK_Left
1435                 if x > 0
1436                     if control_pressed
1437                         $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1438                     end
1439                     if shift_pressed
1440                         move.call('left')
1441                     end
1442                 end
1443                 if alt_pressed
1444                     rotate_and_cleanup.call(-90)
1445                 end
1446             end
1447             if event.keyval == Gdk::Keyval::GDK_Right
1448                 next_ = autotable.get_next_widget(vbox)
1449                 if next_ && autotable.get_current_pos(next_)[0] > x
1450                     if control_pressed
1451                         $vbox2widgets[next_][:textview].grab_focus
1452                     end
1453                     if shift_pressed
1454                         move.call('right')
1455                     end
1456                 end
1457                 if alt_pressed
1458                     rotate_and_cleanup.call(90)
1459                 end
1460             end
1461             if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1462                 delete.call(false)
1463             end
1464             if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1465                 view_element(filename, { :delete => delete })
1466                 propagate = false
1467             end
1468             if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1469                 perform_undo
1470             end
1471             if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1472                 perform_redo
1473             end
1474         end
1475         !propagate  #- propagate if needed
1476     }
1477
1478     $ignore_next_release = false
1479     evtbox.signal_connect('button-press-event') { |w, event|
1480         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1481             if event.state & Gdk::Window::BUTTON3_MASK != 0
1482                 #- gesture redo: hold right mouse button then click left mouse button
1483                 $config['nogestures'] or perform_redo
1484                 $ignore_next_release = true
1485             else
1486                 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1487                 if $r90.active?
1488                     rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1489                 elsif $r270.active?
1490                     rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1491                 elsif $enhance.active?
1492                     enhance_and_cleanup.call
1493                 elsif $delete.active?
1494                     delete.call(false)
1495                 else
1496                     textview.grab_focus
1497                     $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1498                 end
1499             end
1500             $button1_pressed_autotable = true
1501         elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1502             if event.state & Gdk::Window::BUTTON1_MASK != 0
1503                 #- gesture undo: hold left mouse button then click right mouse button
1504                 $config['nogestures'] or perform_undo
1505                 $ignore_next_release = true
1506             end
1507         elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1508             view_element(filename, { :delete => delete })
1509         end
1510         false   #- propagate
1511     }
1512
1513     evtbox.signal_connect('button-release-event') { |w, event|
1514         if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1515             if !$ignore_next_release
1516                 x, y = autotable.get_current_pos(vbox)
1517                 next_ = autotable.get_next_widget(vbox)
1518                 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1519                                      { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1520                                        :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1521                                      { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1522                                        :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1523                                        :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1524                                        :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1525             end
1526             $ignore_next_release = false
1527             $gesture_press = nil
1528         end
1529         false   #- propagate
1530     }
1531
1532     #- handle reordering with drag and drop
1533     Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1534     Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1535     vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1536         selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1537     }
1538
1539     vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1540         done = false
1541         #- mouse gesture first (dnd disables button-release-event)
1542         if $gesture_press && $gesture_press[:filename] == filename
1543             if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1544                 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1545                 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1546                 rotate_and_cleanup.call(angle)
1547                 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1548                 done = true
1549             elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1550                 msg 3, "gesture delete: click-drag right button to the bottom"
1551                 delete.call(false)
1552                 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1553                 done = true
1554             end
1555         end
1556         if !done
1557             ctxt.targets.each { |target|
1558                 if target.name == 'reorder-elements'
1559                     move_dnd = proc { |from,to|
1560                         if from != to
1561                             $modified = true
1562                             autotable.move(from, to)
1563                             save_undo(_("reorder"),
1564                                       proc { |from, to|
1565                                           if to > from
1566                                               autotable.move(to - 1, from)
1567                                           else
1568                                               autotable.move(to, from + 1)
1569                                           end
1570                                           $notebook.set_page(1)
1571                                           proc {
1572                                               autotable.move(from, to)
1573                                               $notebook.set_page(1)
1574                                           }
1575                                       }, from, to)
1576                         end
1577                     }
1578                     if $multiple_dnd.size == 0
1579                         move_dnd.call(selection_data.data.to_i,
1580                                       autotable.get_current_number(vbox))
1581                     else
1582                         UndoHandler.begin_batch
1583                         $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1584                                       each { |path|
1585                             #- need to update current position between each call
1586                             move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1587                                           autotable.get_current_number(vbox))
1588                         }
1589                         UndoHandler.end_batch
1590                     end
1591                     $multiple_dnd = []
1592                 end
1593             }
1594         end
1595     }
1596
1597     vbox.show_all
1598 end
1599
1600 def create_auto_table
1601
1602     $autotable = Gtk::AutoTable.new(5)
1603
1604     $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1605     thumbnails_vb = Gtk::VBox.new(false, 5)
1606
1607     frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1608     $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1609     thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1610     thumbnails_vb.add($autotable)
1611
1612     $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1613     $autotable_sw.add_with_viewport(thumbnails_vb)
1614
1615     #- follows stuff for handling multiple elements selection
1616     press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1617     gc = nil
1618     update_selected = proc {
1619         $autotable.current_order.each { |path|
1620             w = $name2widgets[path][:evtbox].window
1621             xm = w.position[0] + w.size[0]/2
1622             ym = w.position[1] + w.size[1]/2
1623             if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1624                 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1625                     $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1626                     $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1627                 end
1628             end
1629             if $selected_elements[path] && ! $selected_elements[path][:keep]
1630                 if ((xm < press_x && xm < pos_x || xm > pos_x && xm > press_x) || (ym < press_y && ym < pos_y || ym > pos_y && ym > press_y))
1631                     $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1632                     $selected_elements.delete(path)
1633                 end
1634             end
1635         }
1636     }
1637     $autotable.signal_connect('realize') { |w,e|
1638         gc = Gdk::GC.new($autotable.window)
1639         gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1640         gc.function = Gdk::GC::INVERT
1641         #- autoscroll handling for DND and multiple selections
1642         Gtk.timeout_add(100) {
1643             if ! $autotable.window.nil?
1644                 w, x, y, mask = $autotable.window.pointer
1645                 if mask & Gdk::Window::BUTTON1_MASK != 0
1646                     if y < $autotable_sw.vadjustment.value
1647                         if pos_x
1648                             $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1649                         end
1650                         if $button1_pressed_autotable || press_x
1651                             scroll_upper($autotable_sw, y)
1652                         end
1653                         if not press_x.nil?
1654                             w, pos_x, pos_y = $autotable.window.pointer
1655                             $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1656                             update_selected.call
1657                         end
1658                     end
1659                     if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1660                         if pos_x
1661                             $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1662                         end
1663                         if $button1_pressed_autotable || press_x
1664                             scroll_lower($autotable_sw, y)
1665                         end
1666                         if not press_x.nil?
1667                             w, pos_x, pos_y = $autotable.window.pointer
1668                             $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1669                             update_selected.call
1670                         end
1671                     end
1672                 end
1673             end
1674             ! $autotable.window.nil?
1675         }
1676     }
1677
1678     $autotable.signal_connect('button-press-event') { |w,e|
1679         if e.button == 1
1680             if !$button1_pressed_autotable
1681                 press_x = e.x
1682                 press_y = e.y
1683                 if e.state & Gdk::Window::SHIFT_MASK == 0
1684                     $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1685                     $selected_elements = {}
1686                     $statusbar.push(0, utf8(_("Nothing selected.")))
1687                 else
1688                     $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1689                 end
1690                 set_mousecursor(Gdk::Cursor::TCROSS)
1691             end
1692         end
1693     }
1694     $autotable.signal_connect('button-release-event') { |w,e|
1695         if e.button == 1
1696             if $button1_pressed_autotable
1697                 #- unselect all only now
1698                 $multiple_dnd = $selected_elements.keys
1699                 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1700                 $selected_elements = {}
1701                 $button1_pressed_autotable = false
1702             else
1703                 if pos_x
1704                     $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1705                     if $selected_elements.length > 0
1706                         $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1707                     end
1708                 end
1709                 press_x = press_y = pos_x = pos_y = nil
1710                 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1711             end
1712         end
1713     }
1714     $autotable.signal_connect('motion-notify-event') { |w,e|
1715         if ! press_x.nil?
1716             if pos_x
1717                 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1718             end
1719             pos_x = e.x
1720             pos_y = e.y
1721             $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1722             update_selected.call
1723         end
1724     }
1725
1726 end
1727
1728 def create_subalbums_page
1729
1730     subalbums_hb = Gtk::HBox.new
1731     $subalbums_vb = Gtk::VBox.new(false, 5)
1732     subalbums_hb.pack_start($subalbums_vb, false, false)
1733     $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1734     $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1735     $subalbums_sw.add_with_viewport(subalbums_hb)
1736 end
1737
1738 def save_current_file
1739     save_changes
1740
1741     if $filename
1742         begin
1743             begin
1744                 ios = File.open($filename, "w")
1745                 $xmldoc.write(ios, 0)
1746                 ios.close
1747             rescue Iconv::IllegalSequence
1748                 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1749                 if ! ios.nil? && ! ios.closed?
1750                     ios.close
1751                 end
1752                 $xmldoc.xml_decl.encoding = 'UTF-8'
1753                 ios = File.open($filename, "w")
1754                 $xmldoc.write(ios, 0)
1755                 ios.close
1756             end
1757             return true
1758         rescue Exception
1759             puts $!
1760             return false
1761         end
1762     end
1763 end
1764
1765 def save_current_file_user
1766     save_tempfilename = $filename
1767     $filename = $orig_filename
1768     if ! save_current_file
1769         show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1770         $filename = save_tempfilename
1771         return
1772     end
1773     $modified = false
1774     $generated_outofline = false
1775     $filename = save_tempfilename
1776
1777     msg 3, "performing actual deletion of: " + $todelete.join(', ')
1778     $todelete.each { |f|
1779         system("rm -f #{f}")
1780     }
1781 end
1782
1783 def mark_document_as_dirty
1784     $xmldoc.elements.each('//dir') { |elem|
1785         elem.delete_attribute('already-generated')
1786     }
1787 end
1788
1789 #- ret: true => ok  false => cancel
1790 def ask_save_modifications(msg1, msg2, *options)
1791     ret = true
1792     options = options.size > 0 ? options[0] : {}
1793     if $modified
1794         if options[:disallow_cancel]
1795             dialog = Gtk::Dialog.new(msg1,
1796                                      $main_window,
1797                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1798                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1799                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1800         else
1801             dialog = Gtk::Dialog.new(msg1,
1802                                      $main_window,
1803                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1804                                      [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1805                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1806                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1807         end
1808         dialog.default_response = Gtk::Dialog::RESPONSE_YES
1809         dialog.vbox.add(Gtk::Label.new(msg2))
1810         dialog.window_position = Gtk::Window::POS_CENTER
1811         dialog.show_all
1812         
1813         dialog.run { |response|
1814             dialog.destroy
1815             if response == Gtk::Dialog::RESPONSE_YES
1816                 if ! save_current_file_user
1817                     return ask_save_modifications(msg1, msg2, options)
1818                 end
1819             else
1820                 #- if we have generated an album but won't save modifications, we must remove 
1821                 #- already-generated markers in original file
1822                 if $generated_outofline
1823                     begin
1824                         $xmldoc = REXML::Document.new File.new($orig_filename)
1825                         mark_document_as_dirty
1826                         ios = File.open($orig_filename, "w")
1827                         $xmldoc.write(ios, 0)
1828                         ios.close
1829                     rescue Exception
1830                         puts "exception: #{$!}"
1831                     end
1832                 end
1833             end
1834             if response == Gtk::Dialog::RESPONSE_CANCEL
1835                 ret = false
1836             end
1837             $todelete = []  #- unconditionally clear the list of images/videos to delete
1838         }
1839     end
1840     return ret
1841 end
1842
1843 def try_quit(*options)
1844     if ask_save_modifications(utf8(_("Save before quitting?")),
1845                               utf8(_("Do you want to save your changes before quitting?")),
1846                               *options)
1847         Gtk.main_quit
1848     end
1849 end
1850
1851 def show_popup(parent, msg, *options)
1852     dialog = Gtk::Dialog.new
1853     if options[0] && options[0][:title]
1854         dialog.title = options[0][:title]
1855     else
1856         dialog.title = utf8(_("Booh message"))
1857     end
1858     lbl = Gtk::Label.new
1859     if options[0] && options[0][:nomarkup]
1860         lbl.text = msg
1861     else
1862         lbl.markup = msg
1863     end
1864     if options[0] && options[0][:centered]
1865         lbl.set_justify(Gtk::Justification::CENTER)
1866     end
1867     if options[0] && options[0][:selectable]
1868         lbl.selectable = true
1869     end
1870     if options[0] && options[0][:topwidget]
1871         dialog.vbox.add(options[0][:topwidget])
1872     end
1873     if options[0] && options[0][:scrolled]
1874         sw = Gtk::ScrolledWindow.new(nil, nil)
1875         sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1876         sw.add_with_viewport(lbl)
1877         dialog.vbox.add(sw)
1878         dialog.set_default_size(500, 600)
1879     else
1880         dialog.vbox.add(lbl)
1881         dialog.set_default_size(200, 120)
1882     end
1883     if options[0] && options[0][:okcancel]
1884         dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1885     end
1886     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1887
1888     if options[0] && options[0][:pos_centered]
1889         dialog.window_position = Gtk::Window::POS_CENTER
1890     else
1891         dialog.window_position = Gtk::Window::POS_MOUSE
1892     end
1893
1894     if options[0] && options[0][:linkurl]
1895         linkbut = Gtk::Button.new('')
1896         linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1897         linkbut.signal_connect('clicked') {
1898             open_url(options[0][:linkurl] + '/index.html')
1899             dialog.response(Gtk::Dialog::RESPONSE_OK)
1900             set_mousecursor_normal
1901         }
1902         linkbut.relief = Gtk::RELIEF_NONE
1903         linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1904         linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1905         dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1906     end
1907
1908     dialog.show_all
1909
1910     if !options[0] || !options[0][:not_transient]
1911         dialog.transient_for = parent
1912         dialog.run { |response|
1913             dialog.destroy
1914             if options[0] && options[0][:okcancel]
1915                 return response == Gtk::Dialog::RESPONSE_OK
1916             end
1917         }
1918     else
1919         dialog.signal_connect('response') { dialog.destroy }
1920     end
1921 end
1922
1923 def backend_wait_message(parent, msg, infopipe_path, mode)
1924     w = Gtk::Window.new
1925     w.set_transient_for(parent)
1926     w.modal = true
1927
1928     vb = Gtk::VBox.new(false, 5).set_border_width(5)
1929     vb.pack_start(Gtk::Label.new(msg), false, false)
1930
1931     vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1932     vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1933     if mode != 'one dir scan'
1934         vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1935     end
1936     if mode == 'web-album'
1937         vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1938         vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1939     end
1940     vb.pack_start(Gtk::HSeparator.new, false, false)
1941
1942     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1943     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1944     vb.pack_end(bottom, false, false)
1945
1946     infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
1947     refresh_thread = Thread.new {
1948         directories_counter = 0
1949         while line = infopipe.gets
1950             if line =~ /^directories: (\d+), sizes: (\d+)/
1951                 directories = $1.to_f + 1
1952                 sizes = $2.to_f
1953             elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
1954                 elements = $3.to_f + 1
1955                 if mode == 'web-album'
1956                     elements += sizes
1957                 end
1958                 element_counter = 0
1959                 gtk_thread_protect { pb1_1.fraction = 0 }
1960                 if mode != 'one dir scan'
1961                     newtext = utf8(full_src_dir_to_rel($1, $2))
1962                     newtext = '/' if newtext == ''
1963                     gtk_thread_protect { pb1_2.text = newtext }
1964                     directories_counter += 1
1965                     gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
1966                 end
1967             elsif line =~ /^processing element$/
1968                 element_counter += 1
1969                 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1970             elsif line =~ /^processing size$/
1971                 element_counter += 1
1972                 gtk_thread_protect { pb1_1.fraction = element_counter / elements }
1973             elsif line =~ /^finished processing sizes$/
1974                 gtk_thread_protect { pb1_1.fraction = 1 }
1975             elsif line =~ /^creating index.html$/
1976                 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
1977                 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
1978                 directories_counter = 0
1979             elsif line =~ /^index.html: (.+)\|(.+)/
1980                 newtext = utf8(full_src_dir_to_rel($1, $2))
1981                 newtext = '/' if newtext == ''
1982                 gtk_thread_protect { pb2.text = newtext }
1983                 directories_counter += 1
1984                 gtk_thread_protect { pb2.fraction = directories_counter / directories }
1985             elsif line =~ /^die: (.*)$/
1986                 $diemsg = $1
1987             end
1988         end
1989     }
1990
1991     w.add(vb)
1992     w.signal_connect('delete-event') { w.destroy }
1993     w.signal_connect('destroy') {
1994         Thread.kill(refresh_thread)
1995         gtk_thread_flush  #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
1996         if infopipe_path
1997             infopipe.close
1998             system("rm -f #{infopipe_path}")
1999         end
2000     }
2001     w.window_position = Gtk::Window::POS_CENTER
2002     w.show_all
2003
2004     return [ b, w ]
2005 end
2006
2007 def call_backend(cmd, waitmsg, mode, params)
2008     pipe = Tempfile.new("boohpipe")
2009     pipe.close!
2010     system("mkfifo #{pipe.path}")
2011     cmd += " --info-pipe #{pipe.path}"
2012     button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2013     pid = nil
2014     Thread.new {
2015         msg 2, cmd
2016         if pid = fork
2017             id, exitstatus = Process.waitpid2(pid)
2018             gtk_thread_protect { w8.destroy }
2019             if exitstatus == 0
2020                 if params[:successmsg]
2021                     gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2022                 end
2023                 if params[:closure_after]
2024                     gtk_thread_protect(&params[:closure_after])
2025                 end
2026             elsif exitstatus == 15
2027                 #- say nothing, user aborted
2028             else
2029                 gtk_thread_protect { show_popup($main_window,
2030                                                 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2031             end
2032         else
2033             exec(cmd)
2034         end
2035     }
2036     button.signal_connect('clicked') {
2037         Process.kill('SIGTERM', pid)
2038     }
2039 end
2040
2041 def save_changes(*forced)
2042     if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2043         return
2044     end
2045
2046     $xmldir.delete_attribute('already-generated')
2047
2048     propagate_children = proc { |xmldir|
2049         if xmldir.attributes['subdirs-caption']
2050             xmldir.delete_attribute('already-generated')
2051         end
2052         xmldir.elements.each('dir') { |element|
2053             propagate_children.call(element)
2054         }
2055     }
2056
2057     if $xmldir.child_byname_notattr('dir', 'deleted')
2058         new_title = $subalbums_title.buffer.text
2059         if new_title != $xmldir.attributes['subdirs-caption']
2060             parent = $xmldir.parent
2061             if parent.name == 'dir'
2062                 parent.delete_attribute('already-generated')
2063             end
2064             propagate_children.call($xmldir)
2065         end
2066         $xmldir.add_attribute('subdirs-caption', new_title)
2067         $xmldir.elements.each('dir') { |element|
2068             if !element.attributes['deleted']
2069                 path = element.attributes['path']
2070                 newtext = $subalbums_edits[path][:editzone].buffer.text
2071                 if element.attributes['subdirs-caption']
2072                     if element.attributes['subdirs-caption'] != newtext
2073                         propagate_children.call(element)
2074                     end
2075                     element.add_attribute('subdirs-caption',     newtext)
2076                     element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2077                 else
2078                     if element.attributes['thumbnails-caption'] != newtext
2079                         element.delete_attribute('already-generated')
2080                     end
2081                     element.add_attribute('thumbnails-caption',     newtext)
2082                     element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2083                 end
2084             end
2085         }
2086     end
2087
2088     if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2089         if $xmldir.attributes['thumbnails-caption']
2090             path = $xmldir.attributes['path']
2091             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2092         end
2093     elsif $xmldir.attributes['thumbnails-caption']
2094         $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2095     end
2096
2097     if $xmldir.attributes['thumbnails-caption']
2098         if edit = $subalbums_edits[$xmldir.attributes['path']]
2099             $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2100         end
2101     end
2102
2103     #- remove and reinsert elements to reflect new ordering
2104     saves = {}
2105     cpt = 0
2106     $xmldir.elements.each { |element|
2107         if element.name == 'image' || element.name == 'video'
2108             saves[element.attributes['filename']] = element.remove
2109             cpt += 1
2110         end
2111     }
2112     $autotable.current_order.each { |path|
2113         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2114         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2115         saves.delete(path)
2116     }
2117     saves.each_key { |path|
2118         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2119         chld.add_attribute('deleted', 'true')
2120     }
2121 end
2122
2123 def sort_by_exif_date
2124     $modified = true
2125     save_changes
2126     current_order = []
2127     $xmldir.elements.each { |element|
2128         if element.name == 'image' || element.name == 'video'
2129             current_order << element.attributes['filename']
2130         end
2131     }
2132
2133     #- look for EXIF dates
2134     w = Gtk::Window.new
2135     w.set_transient_for($main_window)
2136     w.modal = true
2137     vb = Gtk::VBox.new(false, 5).set_border_width(5)
2138     vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2139     vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2140     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2141     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2142     vb.pack_end(bottom, false, false)
2143     w.add(vb)
2144     w.signal_connect('delete-event') { w.destroy }
2145     w.window_position = Gtk::Window::POS_CENTER
2146     w.show_all
2147
2148     aborted = false
2149     b.signal_connect('clicked') { aborted = true }
2150     dates = {}
2151     i = 0
2152     current_order.each { |f|
2153         i += 1
2154         if entry2type(f) == 'image'
2155             pb.text = f
2156             pb.fraction = i.to_f / current_order.size
2157             Gtk.main_iteration while Gtk.events_pending?
2158             date_time = `identify -format "%[EXIF:DateTimeOriginal]" '#{from_utf8($current_path + "/" + f)}'`.chomp
2159             if $? == 0 && date_time != ''
2160                 dates[f] = date_time
2161             end
2162         end
2163         if aborted
2164             break
2165         end
2166     }
2167     w.destroy
2168     if aborted
2169         return
2170     end
2171
2172     saves = {}
2173     $xmldir.elements.each { |element|
2174         if element.name == 'image' || element.name == 'video'
2175             saves[element.attributes['filename']] = element.remove
2176         end
2177     }
2178
2179     #- find a good fallback for all entries without a date (still next to the item they were next to)
2180     neworder = dates.keys.sort { |a,b| dates[a] <=> dates[b] }
2181     for i in 0 .. current_order.size - 1
2182         if ! neworder.include?(current_order[i])
2183             j = i - 1
2184             while j > 0 && ! neworder.include?(current_order[j])
2185                 j -= 1
2186             end
2187             neworder[(neworder.index(current_order[j]) || -1 ) + 1, 0] = current_order[i]
2188         end
2189     end
2190     neworder.each { |f|
2191         $xmldir.add_element(saves[f].name, saves[f].attributes)
2192     }
2193
2194     #- let the auto-table reflect new ordering
2195     change_dir
2196 end
2197
2198 def remove_all_captions
2199     $modified = true
2200     texts = {}
2201     $autotable.current_order.each { |path|
2202         texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2203         $name2widgets[File.basename(path)][:textview].buffer.text = ''
2204     }
2205     save_undo(_("remove all captions"),
2206               proc { |texts|
2207                   texts.each_key { |key|
2208                       $name2widgets[key][:textview].buffer.text = texts[key]
2209                   }
2210                   $notebook.set_page(1)
2211                   proc {
2212                       texts.each_key { |key|
2213                           $name2widgets[key][:textview].buffer.text = ''
2214                       }
2215                       $notebook.set_page(1)
2216                   }
2217               }, texts)
2218 end
2219
2220 def change_dir
2221     $selected_elements.each_key { |path|
2222         $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2223     }
2224     $autotable.clear
2225     $vbox2widgets = {}
2226     $name2widgets = {}
2227     $name2closures = {}
2228     $selected_elements = {}
2229     $cuts = []
2230     $multiple_dnd = []
2231     UndoHandler.cleanup
2232     $undo_tb.sensitive = $undo_mb.sensitive = false
2233     $redo_tb.sensitive = $redo_mb.sensitive = false
2234
2235     if !$current_path
2236         return
2237     end
2238
2239     $subalbums_vb.children.each { |chld|
2240         $subalbums_vb.remove(chld)
2241     }
2242     $subalbums = Gtk::Table.new(0, 0, true)
2243     current_y_sub_albums = 0
2244
2245     $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2246     $subalbums_edits = {}
2247     subalbums_counter = 0
2248     subalbums_edits_bypos = {}
2249
2250     add_subalbum = proc { |xmldir, counter|
2251         $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2252         subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2253         if xmldir == $xmldir
2254             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2255             captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2256             caption = xmldir.attributes['thumbnails-caption']
2257             infotype = 'thumbnails'
2258         else
2259             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2260             captionfile, caption = find_subalbum_caption_info(xmldir)
2261             infotype = find_subalbum_info_type(xmldir)
2262         end
2263         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2264         hbox = Gtk::HBox.new
2265         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2266         f = Gtk::Frame.new
2267         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2268
2269         img = nil
2270         my_gen_real_thumbnail = proc {
2271             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2272         }
2273
2274         if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2275             f.add(img = Gtk::Image.new)
2276             my_gen_real_thumbnail.call
2277         else
2278             f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2279         end
2280         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2281         $subalbums.attach(hbox,
2282                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2283
2284         frame, textview = create_editzone($subalbums_sw, 0, img)
2285         textview.buffer.text = caption
2286         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2287                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2288
2289         change_image = proc {
2290             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2291                                             nil,
2292                                             Gtk::FileChooser::ACTION_OPEN,
2293                                             nil,
2294                                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2295             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2296             fc.transient_for = $main_window
2297             fc.preview_widget = preview = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(f = Gtk::Frame.new.set_shadow_type(Gtk::SHADOW_ETCHED_OUT))
2298             f.add(preview_img = Gtk::Image.new)
2299             preview.show_all
2300             fc.signal_connect('update-preview') { |w|
2301                 begin
2302                     if fc.preview_filename
2303                         preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2304                         fc.preview_widget_active = true
2305                     end
2306                 rescue Gdk::PixbufError
2307                     fc.preview_widget_active = false
2308                 end
2309             }
2310             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2311                 $modified = true
2312                 old_file = captionfile
2313                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2314                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2315                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2316                 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
2317
2318                 new_file = fc.filename
2319                 msg 3, "new captionfile is: #{fc.filename}"
2320                 perform_changefile = proc {
2321                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2322                     $modified_pixbufs.delete(thumbnail_file)
2323                     xmldir.delete_attribute("#{infotype}-rotate")
2324                     xmldir.delete_attribute("#{infotype}-color-swap")
2325                     xmldir.delete_attribute("#{infotype}-enhance")
2326                     xmldir.delete_attribute("#{infotype}-frame-offset")
2327                     my_gen_real_thumbnail.call
2328                 }
2329                 perform_changefile.call
2330
2331                 save_undo(_("change caption file for sub-album"),
2332                           proc {
2333                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2334                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2335                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2336                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2337                               xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
2338                               my_gen_real_thumbnail.call
2339                               $notebook.set_page(0)
2340                               proc {
2341                                   perform_changefile.call
2342                                   $notebook.set_page(0)
2343                               }
2344                           })
2345             end
2346             fc.destroy
2347         }
2348
2349         refresh = proc {
2350             system("rm -f '#{thumbnail_file}'")
2351             my_gen_real_thumbnail.call
2352         }
2353
2354         rotate_and_cleanup = proc { |angle|
2355             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2356             system("rm -f '#{thumbnail_file}'")
2357         }
2358
2359         move = proc { |direction|
2360             $modified = true
2361
2362             save_changes('forced')
2363             oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2364             if direction == 'up'
2365                 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2366                 subalbums_edits_bypos[oldpos - 1][:position] += 1
2367             end
2368             if direction == 'down'
2369                 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2370                 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2371             end
2372             if direction == 'top'
2373                 for i in 1 .. oldpos - 1
2374                     subalbums_edits_bypos[i][:position] += 1
2375                 end
2376                 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2377             end
2378             if direction == 'bottom'
2379                 for i in oldpos + 1 .. subalbums_counter
2380                     subalbums_edits_bypos[i][:position] -= 1
2381                 end
2382                 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2383             end
2384
2385             elems = []
2386             $xmldir.elements.each('dir') { |element|
2387                 if (!element.attributes['deleted'])
2388                     elems << [ element.attributes['path'], element.remove ]
2389                 end
2390             }
2391             elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2392                   each { |e| $xmldir.add_element(e[1]) }
2393             #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2394             $xmldir.elements.each('descendant::dir') { |elem|
2395                 elem.delete_attribute('already-generated')
2396             }
2397
2398             sel = $albums_tv.selection.selected_rows
2399             change_dir
2400             populate_subalbums_treeview(false)
2401             $albums_tv.selection.select_path(sel[0])
2402         }
2403
2404         color_swap_and_cleanup = proc {
2405             perform_color_swap_and_cleanup = proc {
2406                 color_swap(xmldir, "#{infotype}-")
2407                 my_gen_real_thumbnail.call
2408             }
2409             perform_color_swap_and_cleanup.call
2410
2411             save_undo(_("color swap"),
2412                       proc {
2413                           perform_color_swap_and_cleanup.call
2414                           $notebook.set_page(0)
2415                           proc {
2416                               perform_color_swap_and_cleanup.call
2417                               $notebook.set_page(0)
2418                           }
2419                       })
2420         }
2421
2422         change_frame_offset_and_cleanup = proc {
2423             if values = ask_new_frame_offset(xmldir, "#{infotype}-")
2424                 perform_change_frame_offset_and_cleanup = proc { |val|
2425                     change_frame_offset(xmldir, "#{infotype}-", val)
2426                     my_gen_real_thumbnail.call
2427                 }
2428                 perform_change_frame_offset_and_cleanup.call(values[:new])
2429
2430                 save_undo(_("specify frame offset"),
2431                           proc {
2432                               perform_change_frame_offset_and_cleanup.call(values[:old])
2433                               $notebook.set_page(0)
2434                               proc {
2435                                   perform_change_frame_offset_and_cleanup.call(values[:new])
2436                                   $notebook.set_page(0)
2437                               }
2438                           })
2439             end
2440         }
2441
2442         whitebalance_and_cleanup = proc {
2443             if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2444                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2445                 perform_change_whitebalance_and_cleanup = proc { |val|
2446                     change_whitebalance(xmldir, "#{infotype}-", val)
2447                     recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2448                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2449                     system("rm -f '#{thumbnail_file}'")
2450                 }
2451                 perform_change_whitebalance_and_cleanup.call(values[:new])
2452                 
2453                 save_undo(_("fix white balance"),
2454                           proc {
2455                               perform_change_whitebalance_and_cleanup.call(values[:old])
2456                               $notebook.set_page(0)
2457                               proc {
2458                                   perform_change_whitebalance_and_cleanup.call(values[:new])
2459                                   $notebook.set_page(0)
2460                               }
2461                           })
2462             end
2463         }
2464
2465         gammacorrect_and_cleanup = proc {
2466             if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2467                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2468                 perform_change_gammacorrect_and_cleanup = proc { |val|
2469                     change_gammacorrect(xmldir, "#{infotype}-", val)
2470                     recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2471                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2472                     system("rm -f '#{thumbnail_file}'")
2473                 }
2474                 perform_change_gammacorrect_and_cleanup.call(values[:new])
2475                 
2476                 save_undo(_("gamma correction"),
2477                           proc {
2478                               perform_change_gammacorrect_and_cleanup.call(values[:old])
2479                               $notebook.set_page(0)
2480                               proc {
2481                                   perform_change_gammacorrect_and_cleanup.call(values[:new])
2482                                   $notebook.set_page(0)
2483                               }
2484                           })
2485             end
2486         }
2487
2488         enhance_and_cleanup = proc {
2489             perform_enhance_and_cleanup = proc {
2490                 enhance(xmldir, "#{infotype}-")
2491                 my_gen_real_thumbnail.call
2492             }
2493             
2494             perform_enhance_and_cleanup.call
2495             
2496             save_undo(_("enhance"),
2497                       proc {
2498                           perform_enhance_and_cleanup.call
2499                           $notebook.set_page(0)
2500                           proc {
2501                               perform_enhance_and_cleanup.call
2502                               $notebook.set_page(0)
2503                           }
2504                       })
2505         }
2506
2507         evtbox.signal_connect('button-press-event') { |w, event|
2508             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2509                 if $r90.active?
2510                     rotate_and_cleanup.call(90)
2511                 elsif $r270.active?
2512                     rotate_and_cleanup.call(-90)
2513                 elsif $enhance.active?
2514                     enhance_and_cleanup.call
2515                 end
2516             end
2517             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2518                 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2519                                      { :forbid_left => true, :forbid_right => true,
2520                                        :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2521                                        :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2522                                      { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2523                                        :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2524                                        :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2525             end
2526             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2527                 change_image.call
2528                 true   #- handled
2529             end
2530         }
2531         evtbox.signal_connect('button-press-event') { |w, event|
2532             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2533             false
2534         }
2535
2536         evtbox.signal_connect('button-release-event') { |w, event|
2537             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2538                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2539                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2540                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2541                     msg 3, "gesture rotate: #{angle}"
2542                     rotate_and_cleanup.call(angle)
2543                 end
2544             end
2545             $gesture_press = nil
2546         }
2547                 
2548         $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2549         $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2550         current_y_sub_albums += 1
2551     }
2552
2553     if $xmldir.child_byname_notattr('dir', 'deleted')
2554         #- title edition
2555         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2556         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2557         $subalbums_title.set_justification(Gtk::Justification::CENTER)
2558         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2559         #- this album image/caption
2560         if $xmldir.attributes['thumbnails-caption']
2561             add_subalbum.call($xmldir, 0)
2562         end
2563     end
2564     total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2565     $xmldir.elements.each { |element|
2566         if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2567             #- element (image or video) of this album
2568             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2569             msg 3, "dest_img: #{dest_img}"
2570             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2571             total[element.name] += 1
2572         end
2573         if element.name == 'dir' && !element.attributes['deleted']
2574             #- sub-album image/caption
2575             add_subalbum.call(element, subalbums_counter += 1)
2576             total[element.name] += 1
2577         end
2578     }
2579     $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2580                                                                                 total['image'], total['video'], total['dir'] ]))
2581     $subalbums_vb.add($subalbums)
2582     $subalbums_vb.show_all
2583
2584     if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2585         $notebook.get_tab_label($autotable_sw).sensitive = false
2586         $notebook.set_page(0)
2587         $thumbnails_title.buffer.text = ''
2588     else
2589         $notebook.get_tab_label($autotable_sw).sensitive = true
2590         $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2591     end
2592
2593     if !$xmldir.child_byname_notattr('dir', 'deleted')
2594         $notebook.get_tab_label($subalbums_sw).sensitive = false
2595         $notebook.set_page(1)
2596     else
2597         $notebook.get_tab_label($subalbums_sw).sensitive = true
2598     end
2599 end
2600
2601 def pixbuf_or_nil(filename)
2602     begin
2603         return Gdk::Pixbuf.new(filename)
2604     rescue
2605         return nil
2606     end
2607 end
2608
2609 def theme_choose(current)
2610     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2611                              $main_window,
2612                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2613                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2614                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2615
2616     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2617     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2618     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2619     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2620     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2621     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2622     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2623     treeview.signal_connect('button-press-event') { |w, event|
2624         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2625             dialog.response(Gtk::Dialog::RESPONSE_OK)
2626         end
2627     }
2628
2629     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2630
2631     `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2632         dir.chomp!
2633         iter = model.append
2634         iter[0] = File.basename(dir)
2635         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2636         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2637         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2638         if File.basename(dir) == current
2639             treeview.selection.select_iter(iter)
2640         end
2641     }
2642
2643     dialog.set_default_size(700, 400)
2644     dialog.vbox.show_all
2645     dialog.run { |response|
2646         iter = treeview.selection.selected
2647         dialog.destroy
2648         if response == Gtk::Dialog::RESPONSE_OK && iter
2649             return model.get_value(iter, 0)
2650         end
2651     }
2652     return nil
2653 end
2654
2655 def show_password_protections
2656     examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2657         child_iter = $albums_iters[xmldir.attributes['path']]
2658         if xmldir.attributes['password-protect']
2659             child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2660             already_protected = true
2661         elsif already_protected
2662             pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2663             if pix
2664                 pix = pix.saturate_and_pixelate(1, true)
2665             end
2666             child_iter[2] = pix
2667         else
2668             child_iter[2] = nil
2669         end
2670         xmldir.elements.each('dir') { |elem|
2671             if !elem.attributes['deleted']
2672                 examine_dir_elem.call(child_iter, elem, already_protected)
2673             end
2674         }
2675     }
2676     examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2677 end
2678
2679 def populate_subalbums_treeview(select_first)
2680     $albums_ts.clear
2681     $autotable.clear
2682     $albums_iters = {}
2683     $subalbums_vb.children.each { |chld|
2684         $subalbums_vb.remove(chld)
2685     }
2686
2687     source = $xmldoc.root.attributes['source']
2688     msg 3, "source: #{source}"
2689
2690     xmldir = $xmldoc.elements['//dir']
2691     if !xmldir || xmldir.attributes['path'] != source
2692         msg 1, _("Corrupted booh file...")
2693         return
2694     end
2695
2696     append_dir_elem = proc { |parent_iter, xmldir|
2697         child_iter = $albums_ts.append(parent_iter)
2698         child_iter[0] = File.basename(xmldir.attributes['path'])
2699         child_iter[1] = xmldir.attributes['path']
2700         $albums_iters[xmldir.attributes['path']] = child_iter
2701         msg 3, "puttin location: #{xmldir.attributes['path']}"
2702         xmldir.elements.each('dir') { |elem|
2703             if !elem.attributes['deleted']
2704                 append_dir_elem.call(child_iter, elem)
2705             end
2706         }
2707     }
2708     append_dir_elem.call(nil, xmldir)
2709     show_password_protections
2710
2711     $albums_tv.expand_all
2712     if select_first
2713         $albums_tv.selection.select_iter($albums_ts.iter_first)
2714     end
2715 end
2716
2717 def select_current_theme
2718     select_theme($xmldoc.root.attributes['theme'],
2719                  $xmldoc.root.attributes['limit-sizes'],
2720                  !$xmldoc.root.attributes['optimize-for-32'].nil?,
2721                  $xmldoc.root.attributes['thumbnails-per-row'])
2722 end
2723
2724 def open_file(filename)
2725
2726     $filename = nil
2727     $modified = false
2728     $current_path = nil   #- invalidate
2729     $modified_pixbufs = {}
2730     $albums_ts.clear
2731     $autotable.clear
2732     $subalbums_vb.children.each { |chld|
2733         $subalbums_vb.remove(chld)
2734     }
2735
2736     if !File.exists?(filename)
2737         return utf8(_("File not found."))
2738     end
2739
2740     begin
2741         $xmldoc = REXML::Document.new File.new(filename)
2742     rescue Exception
2743         $xmldoc = nil
2744     end
2745
2746     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2747         if entry2type(filename).nil?
2748             return utf8(_("Not a booh file!"))
2749         else
2750             return utf8(_("Not a booh file!\n\nHint: you cannot import directly an image or video with File/Open.\nUse File/New to create a new album."))
2751         end
2752     end
2753
2754     if !source = $xmldoc.root.attributes['source']
2755         return utf8(_("Corrupted booh file..."))
2756     end
2757
2758     if !dest = $xmldoc.root.attributes['destination']
2759         return utf8(_("Corrupted booh file..."))
2760     end
2761
2762     if !theme = $xmldoc.root.attributes['theme']
2763         return utf8(_("Corrupted booh file..."))
2764     end
2765
2766     if $xmldoc.root.attributes['version'] < '0.8.6'
2767         msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2768         mark_document_as_dirty
2769         if $xmldoc.root.attributes['version'] < '0.8.4'
2770             msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2771             `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2772                 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2773                 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2774                 if old_dest_dir != new_dest_dir
2775                     sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2776                 end
2777                 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2778                     xmldir.elements.each { |element|
2779                         if %w(image video).include?(element.name) && !element.attributes['deleted']
2780                             old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2781                             new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2782                             Dir[old_name + '*'].each { |file|
2783                                 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2784                                 file != new_file and sys("mv '#{file}' '#{new_file}'")
2785                             }
2786                         end
2787                         if element.name == 'dir' && !element.attributes['deleted']
2788                             old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2789                             new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2790                             old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2791                         end
2792                     }
2793                 else
2794                     msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2795                 end
2796             }
2797         end
2798         $xmldoc.root.add_attribute('version', $VERSION)
2799     end
2800
2801     select_current_theme
2802
2803     $filename = filename
2804     $default_size['thumbnails'] =~ /(.*)x(.*)/
2805     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2806     $albums_thumbnail_size =~ /(.*)x(.*)/
2807     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2808
2809     populate_subalbums_treeview(true)
2810
2811     $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
2812     return nil
2813 end
2814
2815 def open_file_user(filename)
2816     result = open_file(filename)
2817     if !result
2818         $config['last-opens'] ||= []
2819         if $config['last-opens'][-1] != utf8(filename)
2820             $config['last-opens'] << utf8(filename)
2821         end
2822         $orig_filename = $filename
2823         tmp = Tempfile.new("boohtemp")
2824         tmp.close!
2825         #- for security
2826         ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2827         ios.close
2828         $tempfiles << $filename << "#{$filename}.backup"
2829     else
2830         $orig_filename = nil
2831     end
2832     return result
2833 end
2834
2835 def open_file_popup
2836     if !ask_save_modifications(utf8(_("Save this album?")),
2837                                utf8(_("Do you want to save the changes to this album?")),
2838                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2839         return
2840     end
2841     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2842                                     nil,
2843                                     Gtk::FileChooser::ACTION_OPEN,
2844                                     nil,
2845                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2846     fc.add_shortcut_folder(File.expand_path("~/.booh"))
2847     fc.set_current_folder(File.expand_path("~/.booh"))
2848     fc.transient_for = $main_window
2849     ok = false
2850     while !ok
2851         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2852             push_mousecursor_wait(fc)
2853             msg = open_file_user(fc.filename)
2854             pop_mousecursor(fc)
2855             if msg
2856                 show_popup(fc, msg)
2857                 ok = false
2858             else
2859                 ok = true
2860             end
2861         else
2862             ok = true
2863         end
2864     end
2865     fc.destroy
2866 end
2867
2868 def open_url(url)
2869     cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
2870     msg 2, cmd
2871     system(cmd)
2872 end
2873
2874 def additional_booh_options
2875     options = ''
2876     if $config['mproc']
2877         options += "--mproc #{$config['mproc'].to_i} "
2878     end
2879     options += "--comments-format '#{$config['comments-format']}'"
2880     return options
2881 end
2882
2883 def new_album
2884     if !ask_save_modifications(utf8(_("Save this album?")),
2885                                utf8(_("Do you want to save the changes to this album?")),
2886                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2887         return
2888     end
2889     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2890                              $main_window,
2891                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2892                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2893                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2894     
2895     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2896     tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2897                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2898     tbl.attach(src = Gtk::Entry.new,
2899                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2900     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2901                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2902     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2903                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2904     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2905                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2906     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2907                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2908     tbl.attach(dest = Gtk::Entry.new,
2909                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2910     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2911                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2912     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2913                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2914     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2915                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2916     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2917                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2918
2919     tooltips = Gtk::Tooltips.new
2920     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2921     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2922                          pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2923     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2924                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
2925     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2926     tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of pictures from non digital cameras are 3/2 when pictures from digital cameras are 4/3)")), nil)
2927     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2928                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2929     nperpage_model = Gtk::ListStore.new(String, String)
2930     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
2931                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
2932     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
2933     nperpagecombo.set_attributes(crt, { :markup => 0 })
2934     iter = nperpage_model.append
2935     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
2936     iter[1] = nil
2937     [ 12, 20, 30, 40, 50 ].each { |v|
2938         iter = nperpage_model.append
2939         iter[0] = iter[1] = v.to_s
2940     }
2941     nperpagecombo.active = 0
2942     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
2943                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
2944     tooltips.set_tip(indexlinkentry, utf8(_("Optional HTML markup to use on pages bottom for a small link returning to wherever you see fit in your website (or somewhere else)")), nil)
2945     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
2946                                    pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
2947     tooltips.set_tip(madewithentry, utf8(_("Optional HTML markup to use on pages bottom for a small 'made with' label; %booh is replaced by the website of booh;\nfor example: made with <a href=%booh>booh</a>!")), nil)
2948
2949     src_nb_calculated_for = ''
2950     src_nb_thread = nil
2951     process_src_nb = proc {
2952         if src.text != src_nb_calculated_for
2953             src_nb_calculated_for = src.text
2954             if src_nb_thread
2955                 Thread.kill(src_nb_thread)
2956                 src_nb_thread = nil
2957             end
2958             if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
2959                 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
2960             else
2961                 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2962                     if File.readable?(from_utf8_safe(src_nb_calculated_for))
2963                         src_nb_thread = Thread.new {
2964                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2965                             total = { 'image' => 0, 'video' => 0, nil => 0 }
2966                             `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2967                                 if File.basename(dir) =~ /^\./
2968                                     next
2969                                 else
2970                                     begin
2971                                         Dir.entries(dir.chomp).each { |file|
2972                                             total[entry2type(file)] += 1
2973                                         }
2974                                     rescue Errno::EACCES, Errno::ENOENT
2975                                     end
2976                                 end
2977                             }
2978                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2979                             src_nb_thread = nil
2980                         }
2981                     else
2982                         src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2983                     end
2984                 else
2985                     src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2986                 end
2987             end
2988         end
2989         true
2990     }
2991     timeout_src_nb = Gtk.timeout_add(100) {
2992         process_src_nb.call
2993     }
2994
2995     src_browse.signal_connect('clicked') {
2996         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2997                                         nil,
2998                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
2999                                         nil,
3000                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3001         fc.transient_for = $main_window
3002         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3003             src.text = utf8(fc.filename)
3004             process_src_nb.call
3005             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3006         end
3007         fc.destroy
3008     }
3009
3010     dest_browse.signal_connect('clicked') {
3011         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3012                                         nil,
3013                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
3014                                         nil,
3015                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3016         fc.transient_for = $main_window
3017         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3018             dest.text = utf8(fc.filename)
3019         end
3020         fc.destroy
3021     }
3022
3023     conf_browse.signal_connect('clicked') {
3024         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3025                                         nil,
3026                                         Gtk::FileChooser::ACTION_SAVE,
3027                                         nil,
3028                                         [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3029         fc.transient_for = $main_window
3030         fc.add_shortcut_folder(File.expand_path("~/.booh"))
3031         fc.set_current_folder(File.expand_path("~/.booh"))
3032         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3033             conf.text = utf8(fc.filename)
3034         end
3035         fc.destroy
3036     }
3037
3038     theme_sizes = []
3039     nperrows = []
3040     recreate_theme_config = proc {
3041         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3042         theme_sizes = []
3043         select_theme(theme_button.label, 'all', optimize432.active?, nil)
3044         $images_size.each { |s|
3045             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3046             if !s['optional']
3047                 cb.active = true
3048             end
3049             tooltips.set_tip(cb, utf8(s['description']), nil)
3050             theme_sizes << { :widget => cb, :value => s['name'] }
3051         }
3052         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3053         tooltips = Gtk::Tooltips.new
3054         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3055         theme_sizes << { :widget => cb, :value => 'original' }
3056         sizes.show_all
3057
3058         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3059         nperrow_group = nil
3060         nperrows = []
3061         $allowed_N_values.each { |n|
3062             if nperrow_group
3063                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3064             else
3065                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3066             end
3067             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3068             if $default_N == n
3069                 rb.active = true
3070             end
3071             nperrows << { :widget => rb, :value => n }
3072         }
3073         nperrowradios.show_all
3074     }
3075     recreate_theme_config.call
3076
3077     theme_button.signal_connect('clicked') {
3078         if newtheme = theme_choose(theme_button.label)
3079             theme_button.label = newtheme
3080             recreate_theme_config.call
3081         end
3082     }
3083
3084     dialog.vbox.add(frame1)
3085     dialog.vbox.add(frame2)
3086     dialog.show_all
3087
3088     keepon = true
3089     ok = true
3090     while keepon
3091         dialog.run { |response|
3092             if response == Gtk::Dialog::RESPONSE_OK
3093                 srcdir = from_utf8_safe(src.text)
3094                 destdir = from_utf8_safe(dest.text)
3095                 confpath = from_utf8_safe(conf.text)
3096                 if src.text != '' && srcdir == ''
3097                     show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3098                     src.grab_focus
3099                 elsif !File.directory?(srcdir)
3100                     show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3101                     src.grab_focus
3102                 elsif dest.text != '' && destdir == ''
3103                     show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3104                     dest.grab_focus
3105                 elsif destdir != make_dest_filename(destdir)
3106                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3107                     dest.grab_focus
3108                 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3109                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
3110                     dest.grab_focus
3111                 elsif File.exists?(destdir) && !File.directory?(destdir)
3112                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3113                     dest.grab_focus
3114                 elsif conf.text == ''
3115                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3116                     conf.grab_focus
3117                 elsif conf.text != '' && confpath == ''
3118                     show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3119                     conf.grab_focus
3120                 elsif File.directory?(confpath)
3121                     show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3122                     conf.grab_focus
3123                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3124                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3125                 else
3126                     system("mkdir '#{destdir}'")
3127                     if !File.directory?(destdir)
3128                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3129                         dest.grab_focus
3130                     else
3131                         keepon = false
3132                     end
3133                 end
3134             else
3135                 keepon = ok = false
3136             end
3137         }
3138     end
3139     if ok
3140         srcdir = from_utf8(src.text)
3141         destdir = from_utf8(dest.text)
3142         configskel = File.expand_path(from_utf8(conf.text))
3143         theme = theme_button.label
3144         sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3145         nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3146         nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3147         opt432 = optimize432.active?
3148         madewith = madewithentry.text.gsub('"', '&quot;').gsub('\'', '&#39;')
3149         indexlink = indexlinkentry.text.gsub('"', '&quot;').gsub('\'', '&#39;')
3150     end
3151     if src_nb_thread
3152         Thread.kill(src_nb_thread)
3153         gtk_thread_flush  #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3154     end
3155     dialog.destroy
3156     Gtk.timeout_remove(timeout_src_nb)
3157
3158     if ok
3159         call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3160                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3161                      (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3162                      "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3163                      utf8(_("Please wait while scanning source directory...")),
3164                      'full scan',
3165                      { :closure_after => proc { open_file_user(configskel) } })
3166     end
3167 end
3168
3169 def properties
3170     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3171                              $main_window,
3172                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3173                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3174                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3175     
3176     source = $xmldoc.root.attributes['source']
3177     dest = $xmldoc.root.attributes['destination']
3178     theme = $xmldoc.root.attributes['theme']
3179     opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3180     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3181     nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3182     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3183     if limit_sizes
3184         limit_sizes = limit_sizes.split(/,/)
3185     end
3186     madewith = $xmldoc.root.attributes['made-with'].gsub('&#39;', '\'')
3187     indexlink = $xmldoc.root.attributes['index-link'].gsub('&#39;', '\'')
3188
3189     tooltips = Gtk::Tooltips.new
3190     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3191     tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3192                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3193     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3194                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3195     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3196                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3197     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3198                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3199     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3200                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3201     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3202                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3203
3204     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3205     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3206                                    pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3207     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3208                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3209     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3210     tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of pictures from non digital cameras are 3/2 when pictures from digital cameras are 4/3)")), nil)
3211     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3212                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3213     nperpage_model = Gtk::ListStore.new(String, String)
3214     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3215                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3216     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3217     nperpagecombo.set_attributes(crt, { :markup => 0 })
3218     iter = nperpage_model.append
3219     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3220     iter[1] = nil
3221     [ 12, 20, 30, 40, 50 ].each { |v|
3222         iter = nperpage_model.append
3223         iter[0] = iter[1] = v.to_s
3224         if nperpage && nperpage == v.to_s
3225             nperpagecombo.active_iter = iter
3226         end
3227     }
3228     if nperpagecombo.active_iter.nil?
3229         nperpagecombo.active = 0
3230     end
3231
3232     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3233                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3234     if indexlink
3235         indexlinkentry.text = indexlink
3236     end
3237     tooltips.set_tip(indexlinkentry, utf8(_("Optional HTML markup to use on pages bottom for a small link returning to wherever you see fit in your website (or somewhere else)")), nil)
3238     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3239                                    pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3240     if madewith
3241         madewithentry.text = madewith
3242     end
3243     tooltips.set_tip(madewithentry, utf8(_('Optional HTML markup to use on pages bottom for a small \'made with\' label; %booh is replaced by the website of booh;\nfor example: made with <a href=%booh>booh</a>!')), nil)
3244
3245     theme_sizes = []
3246     nperrows = []
3247     recreate_theme_config = proc {
3248         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3249         theme_sizes = []
3250         select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3251
3252         $images_size.each { |s|
3253             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
3254             if limit_sizes
3255                 if limit_sizes.include?(s['name'])
3256                     cb.active = true
3257                 end
3258             else
3259                 if !s['optional']
3260                     cb.active = true
3261                 end
3262             end
3263             tooltips.set_tip(cb, utf8(s['description']), nil)
3264             theme_sizes << { :widget => cb, :value => s['name'] }
3265         }
3266         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3267         tooltips = Gtk::Tooltips.new
3268         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3269         if limit_sizes && limit_sizes.include?('original')
3270             cb.active = true
3271         end
3272         theme_sizes << { :widget => cb, :value => 'original' }
3273         sizes.show_all
3274
3275         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3276         nperrow_group = nil
3277         nperrows = []
3278         $allowed_N_values.each { |n|
3279             if nperrow_group
3280                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3281             else
3282                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3283             end
3284             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3285             nperrowradios.add(Gtk::Label.new('  '))
3286             if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3287                 rb.active = true
3288             end
3289             nperrows << { :widget => rb, :value => n.to_s }
3290         }
3291         nperrowradios.show_all
3292     }
3293     recreate_theme_config.call
3294
3295     theme_button.signal_connect('clicked') {
3296         if newtheme = theme_choose(theme_button.label)
3297             limit_sizes = nil
3298             nperrow = nil
3299             theme_button.label = newtheme
3300             recreate_theme_config.call
3301         end
3302     }
3303
3304     dialog.vbox.add(frame1)
3305     dialog.vbox.add(frame2)
3306     dialog.show_all
3307
3308     keepon = true
3309     ok = true
3310     while keepon
3311         dialog.run { |response|
3312             if response == Gtk::Dialog::RESPONSE_OK
3313                 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3314                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3315                 else
3316                     keepon = false
3317                 end
3318             else
3319                 keepon = ok = false
3320             end
3321         }
3322     end
3323     save_theme = theme_button.label
3324     save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3325     save_opt432 = optimize432.active?
3326     save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3327     save_nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3328     save_madewith = madewithentry.text.gsub('"', '&quot;').gsub('\'', '&#39;')
3329     save_indexlink = indexlinkentry.text.gsub('"', '&quot;').gsub('\'', '&#39;')
3330     dialog.destroy
3331
3332     if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_nperpage != nperpage || save_madewith != madewith || save_indexlink != indexlinkentry)
3333         mark_document_as_dirty
3334         save_current_file
3335         call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3336                      "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3337                      (save_nperpage ? "--thumbnails-per-page #{save_nperpage} " : '') +
3338                      "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
3339                      utf8(_("Please wait while scanning source directory...")),
3340                      'full scan',
3341                      { :closure_after => proc {
3342                              open_file($filename)
3343                              $modified = true
3344                          } })
3345     else
3346         #- select_theme merges global variables, need to return to current choices
3347         select_current_theme
3348     end
3349 end
3350
3351 def merge_current
3352     save_current_file
3353
3354     sel = $albums_tv.selection.selected_rows
3355
3356     call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3357                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
3358                  utf8(_("Please wait while scanning source directory...")),
3359                  'one dir scan',
3360                  { :closure_after => proc {
3361                          open_file($filename)
3362                          $albums_tv.selection.select_path(sel[0])
3363                          $modified = true
3364                      } })
3365 end
3366
3367 def merge_newsubs
3368     save_current_file
3369
3370     sel = $albums_tv.selection.selected_rows
3371
3372     call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3373                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
3374                  utf8(_("Please wait while scanning source directory...")),
3375                  'subdirs scan',
3376                  { :closure_after => proc {
3377                          open_file($filename)
3378                          $albums_tv.selection.select_path(sel[0])
3379                          $modified = true
3380                      } })
3381 end
3382
3383 def merge
3384     save_current_file
3385
3386     theme = $xmldoc.root.attributes['theme']
3387     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3388     if limit_sizes
3389         limit_sizes = "--sizes #{limit_sizes}"
3390     end
3391     call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3392                  "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3393                  utf8(_("Please wait while scanning source directory...")),
3394                  'full scan',
3395                  { :closure_after => proc {
3396                          open_file($filename)
3397                          $modified = true
3398                      } })
3399 end
3400
3401 def save_as_do
3402     fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3403                                     nil,
3404                                     Gtk::FileChooser::ACTION_SAVE,
3405                                     nil,
3406                                     [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3407     fc.transient_for = $main_window
3408     fc.add_shortcut_folder(File.expand_path("~/.booh"))
3409     fc.set_current_folder(File.expand_path("~/.booh"))
3410     fc.filename = $orig_filename
3411     if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3412         $orig_filename = fc.filename
3413         if ! save_current_file_user
3414             fc.destroy
3415             return save_as_do
3416         end
3417         $config['last-opens'] ||= []
3418         $config['last-opens'] << $orig_filename
3419     end
3420     fc.destroy
3421 end
3422
3423 def preferences
3424     dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3425                              $main_window,
3426                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3427                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3428                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3429
3430     dialog.vbox.add(notebook = Gtk::Notebook.new)
3431     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3432     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3433                0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3434     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer']).set_size_request(250, -1)),
3435                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3436     tooltips = Gtk::Tooltips.new
3437     tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3438 for example: /usr/bin/mplayer %f")), nil)
3439     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3440                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3441     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3442                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3443     tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3444 for example: /usr/bin/gimp-remote %f")), nil)
3445     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3446                0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3447     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3448                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3449     tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3450 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3451     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3452                0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3453     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(smp_hbox = Gtk::HBox.new.add(smp_spin = Gtk::SpinButton.new(2, 16, 1)).add(Gtk::Label.new(utf8(_("processors")))).set_sensitive(false)),
3454                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3455     tooltips.set_tip(smp_check, utf8(_("When activated, this option allows the thumbnails creation to run faster. However, if you don't have a multi-processor machine, this will only slow down processing!")), nil)
3456     tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3457                0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
3458     tooltips.set_tip(nogestures_check, utf8(_("Mouse gestures are 'unusual' mouse movements triggering special actions, and are great for speeding up your editions. Get details on available mouse gestures from the Help menu.")), nil)
3459     tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original images/videos as well"))),
3460                0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
3461     tooltips.set_tip(deleteondisk_check, utf8(_("Normally, deleting an image or video in booh only removes it from the web-album. If you check this option, the original file in source directory will be removed as well. Undo is possible, since actual deletion is performed only when web-album is saved.")), nil)
3462
3463     smp_check.signal_connect('toggled') {
3464         if smp_check.active?
3465             smp_hbox.sensitive = true
3466         else
3467             smp_hbox.sensitive = false
3468         end
3469     }
3470     if $config['mproc']
3471         smp_check.active = true
3472         smp_spin.value = $config['mproc'].to_i
3473     end
3474     nogestures_check.active = $config['nogestures']
3475     deleteondisk_check.active = $config['deleteondisk']
3476
3477     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
3478     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
3479                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3480     tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
3481                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3482     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nimages in new albums:"))),
3483                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3484     tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
3485                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3486     tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
3487                2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3488     tooltips.set_tip(commentsformat_entry, utf8(_("Normally, filenames without extension are used as comments for images and videos in new albums. Use this entry to use something else for images.")), nil)
3489     commentsformat_help.signal_connect('clicked') {
3490         show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
3491 hence you should look at ImageMagick/identify documentation for the most    
3492 accurate and up-to-date documentation. Last time I checked, documentation
3493 was:
3494
3495 Print information about the image in a format of your choosing. You can
3496 include the image filename, type, width, height, Exif data, or other image
3497 attributes by embedding special format characters:                          
3498
3499                      %O   page offset
3500                      %P   page width and height                             
3501                      %b   file size                                         
3502                      %c   comment                                           
3503                      %d   directory                                         
3504                      %e   filename extension                                
3505                      %f   filename                                          
3506                      %g   page geometry                                     
3507                      %h   height                                            
3508                      %i   input filename                                    
3509                      %k   number of unique colors                           
3510                      %l   label                                             
3511                      %m   magick                                            
3512                      %n   number of scenes                                  
3513                      %o   output filename                                   
3514                      %p   page number                                       
3515                      %q   quantum depth                                     
3516                      %r   image class and colorspace                        
3517                      %s   scene number                                      
3518                      %t   top of filename                                   
3519                      %u   unique temporary filename                         
3520                      %w   width                                             
3521                      %x   x resolution                                      
3522                      %y   y resolution                                      
3523                      %z   image depth                                       
3524                      %@   bounding box                                      
3525                      %#   signature                                         
3526                      %%   a percent sign                                    
3527                                                                             
3528 For example,                                                                
3529                                                                             
3530     %m:%f %wx%h
3531                                                                             
3532 displays MIFF:bird.miff 512x480 for an image titled bird.miff and whose
3533 width is 512 and height is 480.                
3534                                                                             
3535 If the first character of string is @, the format is read from a file titled
3536 by the remaining characters in the string.
3537                                                                             
3538 You can also use the following special formatting syntax to print Exif
3539 information contained in the file:
3540                                                                             
3541     %[EXIF:tag]                                                             
3542                                                                             
3543 Where tag can be one of the following:                                      
3544                                                                             
3545     *  (print all Exif tags, in keyword=data format)                        
3546     !  (print all Exif tags, in tag_number data format)                     
3547     #hhhh (print data for Exif tag #hhhh)                                   
3548     ImageWidth                                                              
3549     ImageLength                                                             
3550     BitsPerSample                                                           
3551     Compression                                                             
3552     PhotometricInterpretation                                               
3553     FillOrder                                                               
3554     DocumentName                                                            
3555     ImageDescription                                                        
3556     Make                                                                    
3557     Model                                                                   
3558     StripOffsets                                                            
3559     Orientation                                                             
3560     SamplesPerPixel                                                         
3561     RowsPerStrip                                                            
3562     StripByteCounts                                                         
3563     XResolution                                                             
3564     YResolution                                                             
3565     PlanarConfiguration                                                     
3566     ResolutionUnit                                                          
3567     TransferFunction                                                        
3568     Software                                                                
3569     DateTime                                                                
3570     Artist                                                                  
3571     WhitePoint                                                              
3572     PrimaryChromaticities                                                   
3573     TransferRange                                                           
3574     JPEGProc                                                                
3575     JPEGInterchangeFormat                                                   
3576     JPEGInterchangeFormatLength                                             
3577     YCbCrCoefficients                                                       
3578     YCbCrSubSampling                                                        
3579     YCbCrPositioning                                                        
3580     ReferenceBlackWhite                                                     
3581     CFARepeatPatternDim                                                     
3582     CFAPattern                                                              
3583     BatteryLevel                                                            
3584     Copyright                                                               
3585     ExposureTime                                                            
3586     FNumber                                                                 
3587     IPTC/NAA                                                                
3588     ExifOffset                                                              
3589     InterColorProfile                                                       
3590     ExposureProgram                                                         
3591     SpectralSensitivity                                                     
3592     GPSInfo                                                                 
3593     ISOSpeedRatings                                                         
3594     OECF                                                                    
3595     ExifVersion                                                             
3596     DateTimeOriginal                                                        
3597     DateTimeDigitized                                                       
3598     ComponentsConfiguration                                                 
3599     CompressedBitsPerPixel                                                  
3600     ShutterSpeedValue                                                       
3601     ApertureValue                                                           
3602     BrightnessValue                                                         
3603     ExposureBiasValue                                                       
3604     MaxApertureValue                                                        
3605     SubjectDistance                                                         
3606     MeteringMode                                                            
3607     LightSource                                                             
3608     Flash                                                                   
3609     FocalLength                                                             
3610     MakerNote                                                               
3611     UserComment                                                             
3612     SubSecTime                                                              
3613     SubSecTimeOriginal                                                      
3614     SubSecTimeDigitized                                                     
3615     FlashPixVersion                                                         
3616     ColorSpace                                                              
3617     ExifImageWidth                                                          
3618     ExifImageLength                                                         
3619     InteroperabilityOffset                                                  
3620     FlashEnergy                                                             
3621     SpatialFrequencyResponse                                                
3622     FocalPlaneXResolution                                                   
3623     FocalPlaneYResolution                                                   
3624     FocalPlaneResolutionUnit                                                
3625     SubjectLocation                                                         
3626     ExposureIndex                                                           
3627     SensingMethod                                                           
3628     FileSource                                                              
3629     SceneType")), { :scrolled => true })
3630     }
3631
3632     dialog.vbox.show_all
3633     dialog.run { |response|
3634         if response == Gtk::Dialog::RESPONSE_OK
3635             $config['video-viewer'] = from_utf8(video_viewer_entry.text)
3636             $config['image-editor'] = from_utf8(image_editor_entry.text)
3637             $config['browser'] = from_utf8(browser_entry.text)
3638             if smp_check.active?
3639                 $config['mproc'] = smp_spin.value.to_i
3640             else
3641                 $config.delete('mproc')
3642             end
3643             $config['nogestures'] = nogestures_check.active?
3644             $config['deleteondisk'] = deleteondisk_check.active?
3645
3646             $config['convert-enhance'] = from_utf8(enhance_entry.text)
3647             $config['comments-format'] = from_utf8(commentsformat_entry.text.gsub(/'/, ''))
3648         end
3649     }
3650     dialog.destroy
3651 end
3652
3653 def perform_undo
3654     if $undo_tb.sensitive?
3655         $redo_tb.sensitive = $redo_mb.sensitive = true
3656         if not more_undoes = UndoHandler.undo($statusbar)
3657             $undo_tb.sensitive = $undo_mb.sensitive = false
3658         end
3659     end
3660 end
3661
3662 def perform_redo
3663     if $redo_tb.sensitive?
3664         $undo_tb.sensitive = $undo_mb.sensitive = true
3665         if not more_redoes = UndoHandler.redo($statusbar)
3666             $redo_tb.sensitive = $redo_mb.sensitive = false
3667         end
3668     end
3669 end
3670
3671 def show_one_click_explanation(intro)
3672     show_popup($main_window, utf8(_("<b>One-Click tools.</b>
3673
3674 %s When such a tool is activated
3675 (<span foreground='darkblue'>Rotate clockwise</span>, <span foreground='darkblue'>Rotate counter-clockwise</span>, <span foreground='darkblue'>Enhance</span> or <span foreground='darkblue'>Delete</span>), clicking
3676 on a thumbnail will immediately apply the desired action.
3677
3678 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
3679 ") % intro), { :pos_centered => true })
3680 end
3681
3682 def get_license
3683     return <<"EOF"
3684                     GNU GENERAL PUBLIC LICENSE
3685                        Version 2, June 1991
3686
3687  Copyright (C) 1989, 1991 Free Software Foundation, Inc.
3688                           675 Mass Ave, Cambridge, MA 02139, USA
3689  Everyone is permitted to copy and distribute verbatim copies
3690  of this license document, but changing it is not allowed.
3691
3692                             Preamble
3693
3694   The licenses for most software are designed to take away your
3695 freedom to share and change it.  By contrast, the GNU General Public
3696 License is intended to guarantee your freedom to share and change free
3697 software--to make sure the software is free for all its users.  This
3698 General Public License applies to most of the Free Software
3699 Foundation's software and to any other program whose authors commit to
3700 using it.  (Some other Free Software Foundation software is covered by
3701 the GNU Library General Public License instead.)  You can apply it to
3702 your programs, too.
3703
3704   When we speak of free software, we are referring to freedom, not
3705 price.  Our General Public Licenses are designed to make sure that you
3706 have the freedom to distribute copies of free software (and charge for
3707 this service if you wish), that you receive source code or can get it
3708 if you want it, that you can change the software or use pieces of it
3709 in new free programs; and that you know you can do these things.
3710
3711   To protect your rights, we need to make restrictions that forbid
3712 anyone to deny you these rights or to ask you to surrender the rights.
3713 These restrictions translate to certain responsibilities for you if you
3714 distribute copies of the software, or if you modify it.
3715
3716   For example, if you distribute copies of such a program, whether
3717 gratis or for a fee, you must give the recipients all the rights that
3718 you have.  You must make sure that they, too, receive or can get the
3719 source code.  And you must show them these terms so they know their
3720 rights.
3721
3722   We protect your rights with two steps: (1) copyright the software, and
3723 (2) offer you this license which gives you legal permission to copy,
3724 distribute and/or modify the software.
3725
3726   Also, for each author's protection and ours, we want to make certain
3727 that everyone understands that there is no warranty for this free
3728 software.  If the software is modified by someone else and passed on, we
3729 want its recipients to know that what they have is not the original, so
3730 that any problems introduced by others will not reflect on the original
3731 authors' reputations.
3732
3733   Finally, any free program is threatened constantly by software
3734 patents.  We wish to avoid the danger that redistributors of a free
3735 program will individually obtain patent licenses, in effect making the
3736 program proprietary.  To prevent this, we have made it clear that any
3737 patent must be licensed for everyone's free use or not licensed at all.
3738
3739   The precise terms and conditions for copying, distribution and
3740 modification follow.
3741
3742
3743                     GNU GENERAL PUBLIC LICENSE
3744    TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
3745
3746   0. This License applies to any program or other work which contains
3747 a notice placed by the copyright holder saying it may be distributed
3748 under the terms of this General Public License.  The "Program", below,
3749 refers to any such program or work, and a "work based on the Program"
3750 means either the Program or any derivative work under copyright law:
3751 that is to say, a work containing the Program or a portion of it,
3752 either verbatim or with modifications and/or translated into another
3753 language.  (Hereinafter, translation is included without limitation in
3754 the term "modification".)  Each licensee is addressed as "you".
3755
3756 Activities other than copying, distribution and modification are not
3757 covered by this License; they are outside its scope.  The act of
3758 running the Program is not restricted, and the output from the Program
3759 is covered only if its contents constitute a work based on the
3760 Program (independent of having been made by running the Program).
3761 Whether that is true depends on what the Program does.
3762
3763   1. You may copy and distribute verbatim copies of the Program's
3764 source code as you receive it, in any medium, provided that you
3765 conspicuously and appropriately publish on each copy an appropriate
3766 copyright notice and disclaimer of warranty; keep intact all the
3767 notices that refer to this License and to the absence of any warranty;
3768 and give any other recipients of the Program a copy of this License
3769 along with the Program.
3770
3771 You may charge a fee for the physical act of transferring a copy, and
3772 you may at your option offer warranty protection in exchange for a fee.
3773
3774   2. You may modify your copy or copies of the Program or any portion
3775 of it, thus forming a work based on the Program, and copy and
3776 distribute such modifications or work under the terms of Section 1
3777 above, provided that you also meet all of these conditions:
3778
3779     a) You must cause the modified files to carry prominent notices
3780     stating that you changed the files and the date of any change.
3781
3782     b) You must cause any work that you distribute or publish, that in
3783     whole or in part contains or is derived from the Program or any
3784     part thereof, to be licensed as a whole at no charge to all third
3785     parties under the terms of this License.
3786
3787     c) If the modified program normally reads commands interactively
3788     when run, you must cause it, when started running for such
3789     interactive use in the most ordinary way, to print or display an
3790     announcement including an appropriate copyright notice and a
3791     notice that there is no warranty (or else, saying that you provide
3792     a warranty) and that users may redistribute the program under
3793     these conditions, and telling the user how to view a copy of this
3794     License.  (Exception: if the Program itself is interactive but
3795     does not normally print such an announcement, your work based on
3796     the Program is not required to print an announcement.)
3797
3798
3799 These requirements apply to the modified work as a whole.  If
3800 identifiable sections of that work are not derived from the Program,
3801 and can be reasonably considered independent and separate works in
3802 themselves, then this License, and its terms, do not apply to those
3803 sections when you distribute them as separate works.  But when you
3804 distribute the same sections as part of a whole which is a work based
3805 on the Program, the distribution of the whole must be on the terms of
3806 this License, whose permissions for other licensees extend to the
3807 entire whole, and thus to each and every part regardless of who wrote it.
3808
3809 Thus, it is not the intent of this section to claim rights or contest
3810 your rights to work written entirely by you; rather, the intent is to
3811 exercise the right to control the distribution of derivative or
3812 collective works based on the Program.
3813
3814 In addition, mere aggregation of another work not based on the Program
3815 with the Program (or with a work based on the Program) on a volume of
3816 a storage or distribution medium does not bring the other work under
3817 the scope of this License.
3818
3819   3. You may copy and distribute the Program (or a work based on it,
3820 under Section 2) in object code or executable form under the terms of
3821 Sections 1 and 2 above provided that you also do one of the following:
3822
3823     a) Accompany it with the complete corresponding machine-readable
3824     source code, which must be distributed under the terms of Sections
3825     1 and 2 above on a medium customarily used for software interchange; or,
3826
3827     b) Accompany it with a written offer, valid for at least three
3828     years, to give any third party, for a charge no more than your
3829     cost of physically performing source distribution, a complete
3830     machine-readable copy of the corresponding source code, to be
3831     distributed under the terms of Sections 1 and 2 above on a medium
3832     customarily used for software interchange; or,
3833
3834     c) Accompany it with the information you received as to the offer
3835     to distribute corresponding source code.  (This alternative is
3836     allowed only for noncommercial distribution and only if you
3837     received the program in object code or executable form with such
3838     an offer, in accord with Subsection b above.)
3839
3840 The source code for a work means the preferred form of the work for
3841 making modifications to it.  For an executable work, complete source
3842 code means all the source code for all modules it contains, plus any
3843 associated interface definition files, plus the scripts used to
3844 control compilation and installation of the executable.  However, as a
3845 special exception, the source code distributed need not include
3846 anything that is normally distributed (in either source or binary
3847 form) with the major components (compiler, kernel, and so on) of the
3848 operating system on which the executable runs, unless that component
3849 itself accompanies the executable.
3850
3851 If distribution of executable or object code is made by offering
3852 access to copy from a designated place, then offering equivalent
3853 access to copy the source code from the same place counts as
3854 distribution of the source code, even though third parties are not
3855 compelled to copy the source along with the object code.
3856
3857
3858   4. You may not copy, modify, sublicense, or distribute the Program
3859 except as expressly provided under this License.  Any attempt
3860 otherwise to copy, modify, sublicense or distribute the Program is
3861 void, and will automatically terminate your rights under this License.
3862 However, parties who have received copies, or rights, from you under
3863 this License will not have their licenses terminated so long as such
3864 parties remain in full compliance.
3865
3866   5. You are not required to accept this License, since you have not
3867 signed it.  However, nothing else grants you permission to modify or
3868 distribute the Program or its derivative works.  These actions are
3869 prohibited by law if you do not accept this License.  Therefore, by
3870 modifying or distributing the Program (or any work based on the
3871 Program), you indicate your acceptance of this License to do so, and
3872 all its terms and conditions for copying, distributing or modifying
3873 the Program or works based on it.
3874
3875   6. Each time you redistribute the Program (or any work based on the
3876 Program), the recipient automatically receives a license from the
3877 original licensor to copy, distribute or modify the Program subject to
3878 these terms and conditions.  You may not impose any further
3879 restrictions on the recipients' exercise of the rights granted herein.
3880 You are not responsible for enforcing compliance by third parties to
3881 this License.
3882
3883   7. If, as a consequence of a court judgment or allegation of patent
3884 infringement or for any other reason (not limited to patent issues),
3885 conditions are imposed on you (whether by court order, agreement or
3886 otherwise) that contradict the conditions of this License, they do not
3887 excuse you from the conditions of this License.  If you cannot
3888 distribute so as to satisfy simultaneously your obligations under this
3889 License and any other pertinent obligations, then as a consequence you
3890 may not distribute the Program at all.  For example, if a patent
3891 license would not permit royalty-free redistribution of the Program by
3892 all those who receive copies directly or indirectly through you, then
3893 the only way you could satisfy both it and this License would be to
3894 refrain entirely from distribution of the Program.
3895
3896 If any portion of this section is held invalid or unenforceable under
3897 any particular circumstance, the balance of the section is intended to
3898 apply and the section as a whole is intended to apply in other
3899 circumstances.
3900
3901 It is not the purpose of this section to induce you to infringe any
3902 patents or other property right claims or to contest validity of any
3903 such claims; this section has the sole purpose of protecting the
3904 integrity of the free software distribution system, which is
3905 implemented by public license practices.  Many people have made
3906 generous contributions to the wide range of software distributed
3907 through that system in reliance on consistent application of that
3908 system; it is up to the author/donor to decide if he or she is willing
3909 to distribute software through any other system and a licensee cannot
3910 impose that choice.
3911
3912 This section is intended to make thoroughly clear what is believed to
3913 be a consequence of the rest of this License.
3914
3915
3916   8. If the distribution and/or use of the Program is restricted in
3917 certain countries either by patents or by copyrighted interfaces, the
3918 original copyright holder who places the Program under this License
3919 may add an explicit geographical distribution limitation excluding
3920 those countries, so that distribution is permitted only in or among
3921 countries not thus excluded.  In such case, this License incorporates
3922 the limitation as if written in the body of this License.
3923
3924   9. The Free Software Foundation may publish revised and/or new versions
3925 of the General Public License from time to time.  Such new versions will
3926 be similar in spirit to the present version, but may differ in detail to
3927 address new problems or concerns.
3928
3929 Each version is given a distinguishing version number.  If the Program
3930 specifies a version number of this License which applies to it and "any
3931 later version", you have the option of following the terms and conditions
3932 either of that version or of any later version published by the Free
3933 Software Foundation.  If the Program does not specify a version number of
3934 this License, you may choose any version ever published by the Free Software
3935 Foundation.
3936
3937   10. If you wish to incorporate parts of the Program into other free
3938 programs whose distribution conditions are different, write to the author
3939 to ask for permission.  For software which is copyrighted by the Free
3940 Software Foundation, write to the Free Software Foundation; we sometimes
3941 make exceptions for this.  Our decision will be guided by the two goals
3942 of preserving the free status of all derivatives of our free software and
3943 of promoting the sharing and reuse of software generally.
3944
3945                             NO WARRANTY
3946
3947   11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
3948 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
3949 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
3950 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
3951 OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
3952 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
3953 TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
3954 PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
3955 REPAIR OR CORRECTION.
3956
3957   12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
3958 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
3959 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
3960 INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
3961 OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
3962 TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
3963 YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
3964 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
3965 POSSIBILITY OF SUCH DAMAGES.
3966 EOF
3967 end
3968
3969 def create_menu_and_toolbar
3970     
3971     #- menu
3972     mb = Gtk::MenuBar.new
3973
3974     filemenu = Gtk::MenuItem.new(utf8(_("_File")))
3975     filesubmenu = Gtk::Menu.new
3976     filesubmenu.append(new       = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
3977     filesubmenu.append(open      = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
3978     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
3979     filesubmenu.append($save     = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
3980     filesubmenu.append($save_as  = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
3981     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
3982     tooltips = Gtk::Tooltips.new
3983     filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
3984     $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3985     tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
3986     filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
3987     $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3988     tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
3989     filesubmenu.append($merge    = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
3990     $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
3991     tooltips.set_tip($merge, utf8(_("Take