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