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