fa6c35413e41ba5de94cea9964888fada3fa4d82
[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 <gc3 at bluewin.ch>
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
25 require 'gtk2'
26 require 'booh/GtkAutoTable'
27
28 require 'gettext'
29 include GetText
30 bindtextdomain("booh")
31
32 require 'rexml/document'
33 include REXML
34
35 require 'booh/booh-lib'
36 include Booh
37 require 'booh/UndoHandler'
38
39
40 #- options
41 $options = [
42     [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       _("Get help message") ],
43
44     [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
45 ]
46
47 def usage
48     puts _("Usage: %s [OPTION]...") % File.basename($0)
49     $options.each { |ary|
50         printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
51     }
52 end
53
54 def handle_options
55     parser = GetoptLong.new
56     parser.set_options(*$options.collect { |ary| ary[0..2] })
57     begin
58         parser.each_option do |name, arg|
59             case name
60             when '--help'
61                 usage
62                 exit(0)
63
64             when '--verbose-level'
65                 $verbose_level = arg.to_i
66
67             end
68         end
69     rescue
70         puts $!
71         usage
72         exit(1)
73     end
74 end
75
76 def read_config
77     $config = {}
78     $config_file = File.expand_path('~/.booh-gui-rc')
79     if File.readable?($config_file)
80         $xmldoc = REXML::Document.new(File.new($config_file))
81         $xmldoc.root.elements.each { |element|
82             txt = element.get_text
83             if txt 
84                 if txt.value =~ /~~~/ || element.name == 'last-opens'
85                     $config[element.name] = txt.value.split(/~~~/)
86                 else
87                     $config[element.name] = txt.value
88                 end
89             else
90                 $config[element.name] = {}
91                 element.each { |chld|
92                     txt = chld.get_text
93                     $config[element.name][chld.name] = txt ? txt.value : nil
94                 }
95             end
96         }
97     end
98     $config['video-viewer'] ||= 'mplayer %f'
99     if !FileTest.directory?(File.expand_path('~/.booh'))
100         system("mkdir ~/.booh")
101     end
102 end
103
104 def write_config
105     if $config['last-opens'] && $config['last-opens'].size > 5
106         $config['last-opens'] = $config['last-opens'][-5, 5]
107     end
108
109     ios = File.open($config_file, "w")
110     $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
111     $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
112     $config.each_pair { |key, value|
113         elem = $xmldoc.root.add_element key
114         if value.is_a? Hash
115             $config[key].each_pair { |subkey, subvalue|
116                 subelem = elem.add_element subkey
117                 subelem.add_text subvalue.to_s
118             }
119         elsif value.is_a? Array
120             elem.add_text value.join('~~~')
121         else
122             elem.add_text value.to_s
123         end
124     }
125     $xmldoc.write(ios, 0)
126     ios.close
127 end
128
129 def set_mousecursor(what, *widget)
130     if widget[0] && widget[0].window
131         widget[0].window.set_cursor(Gdk::Cursor.new(what))
132     end
133     if $main_window.window
134         $main_window.window.set_cursor(Gdk::Cursor.new(what))
135     end
136     $current_cursor = what
137 end
138 def set_mousecursor_wait(*widget)
139     set_mousecursor(Gdk::Cursor::WATCH, *widget)
140     if Thread.current == Thread.main
141         Gtk.main_iteration while Gtk.events_pending?
142     end
143 end
144 def set_mousecursor_normal(*widget)
145     set_mousecursor(Gdk::Cursor::LEFT_PTR, *widget)
146 end
147 def push_mousecursor_wait(*widget)
148     if $current_cursor != Gdk::Cursor::WATCH
149         $save_cursor = $current_cursor
150         set_mousecursor_wait(*widget)
151     end
152 end
153 def pop_mousecursor(*widget)
154     set_mousecursor($save_cursor || Gdk::Cursor::LEFT_PTR, *widget)
155 end
156
157 def current_dest_dir
158     source = $xmldoc.root.attributes['source']
159     dest = $xmldoc.root.attributes['destination']
160     return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
161 end
162
163 def build_full_dest_filename(filename)
164     return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
165 end
166
167 def save_undo(name, closure, *params)
168     UndoHandler.save_undo(name, closure, [ *params ])
169     $undo_tb.sensitive = $undo_mb.sensitive = true
170 end
171
172 def view_element(filename)
173     if entry2type(filename) == 'video'
174         cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'")
175         msg 2, cmd
176         system(cmd)
177         return
178     end
179
180     w = Gtk::Window.new
181
182     msg 3, "filename: #{filename}"
183     dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
184     #- typically this file won't exist in case of videos; try with the largest thumbnail around
185     if !File.exists?(dest_img)
186         if entry2type(filename) == 'video'
187             alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
188             if not alternatives.empty?
189                 dest_img = alternatives[-1]
190             end
191         else
192             push_mousecursor_wait
193             gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
194             pop_mousecursor
195             if !File.exists?(dest_img)
196                 msg 2, _("Could not generate fullscreen thumbnail!")
197                 return
198                 end
199         end
200     end
201     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)))
202
203     tooltips = Gtk::Tooltips.new
204     tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
205
206     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
207     b.signal_connect('clicked') { w.destroy }
208
209     vb = Gtk::VBox.new
210     vb.pack_start(evt, false, false)
211     vb.pack_end(bottom, false, false)
212
213     w.add(vb)
214     w.signal_connect('delete-event') { w.destroy }
215     w.window_position = Gtk::Window::POS_CENTER
216     w.show_all
217 end
218
219 def create_editzone(scrolledwindow, pagenum, image)
220     frame = Gtk::Frame.new
221     frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
222     frame.set_shadow_type(Gtk::SHADOW_IN)
223     textview.signal_connect('key-press-event') { |w, event|
224         textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
225         if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
226             scrolledwindow.signal_emit('key-press-event', event)
227         end
228         if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
229            event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
230             if event.keyval == Gdk::Keyval::GDK_Up
231                 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
232                     scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
233                 else
234                     scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
235                 end
236             else
237                 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
238                     scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
239                 else
240                     scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
241                 end
242             end
243         end
244         false  #- propagate
245     }
246     textview.signal_connect('focus-in-event') { |w, event|
247         textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
248         false  #- propagate
249     }
250
251     candidate_undo_text = nil
252     textview.signal_connect('focus-in-event') { |w, event|
253         candidate_undo_text = textview.buffer.text
254         false  #- propagate
255     }
256     textview.signal_connect('key-release-event') { |w, event|
257         if candidate_undo_text && candidate_undo_text != textview.buffer.text
258             $modified = true
259             save_undo(_("text edit"),
260                       Proc.new { |text|
261                           save_text = textview.buffer.text
262                           textview.buffer.text = text
263                           textview.grab_focus
264                           $notebook.set_page(pagenum)
265                           Proc.new {
266                               textview.buffer.text = save_text
267                               textview.grab_focus
268                               $notebook.set_page(pagenum)
269                           }
270                       }, candidate_undo_text)
271             candidate_undo_text = nil
272         end
273
274         if ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
275             #- autoscroll if cursor or image is not visible
276             ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
277             ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
278             current_miny_visible = scrolledwindow.vadjustment.value
279             current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
280             if ypos_top < current_miny_visible
281                 newval = scrolledwindow.vadjustment.value -
282                          ((current_miny_visible - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
283                 if newval < scrolledwindow.vadjustment.lower
284                     newval = scrolledwindow.vadjustment.lower
285                 end
286                 scrolledwindow.vadjustment.value = newval
287             elsif ypos_bottom > current_maxy_visible
288                 newval = scrolledwindow.vadjustment.value +
289                          ((ypos_bottom - current_maxy_visible - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
290                 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
291                     newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
292                 end
293                 scrolledwindow.vadjustment.value = newval
294             end
295         end
296         false  #- propagate
297     }
298
299     return [ frame, textview ]
300 end
301
302 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
303     $modified = true
304
305     #- update rotate attribute
306     xmlelem.add_attribute("#{attributes_prefix}rotate", current_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
307
308     if !$rotated_pixbufs[thumbnail_img]
309         $rotated_pixbufs[thumbnail_img] = { :orig => img.pixbuf, :angle_to_orig => angle % 360 }
310     else
311         $rotated_pixbufs[thumbnail_img][:angle_to_orig] = ($rotated_pixbufs[thumbnail_img][:angle_to_orig] + angle) % 360
312     end
313     msg 3, "angle: #{angle}, angle to orig: #{$rotated_pixbufs[thumbnail_img][:angle_to_orig]}"
314
315     #- rotate shown thumbnail
316     pixbuf = rotate_pixbuf($rotated_pixbufs[thumbnail_img][:orig], $rotated_pixbufs[thumbnail_img][:angle_to_orig])
317     msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
318     if pixbuf.height > desired_y
319         img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y,
320                                                                              Gdk::Pixbuf::INTERP_BILINEAR)
321     elsif pixbuf.width < desired_x && pixbuf.height < desired_y
322         img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width),
323                                                                              Gdk::Pixbuf::INTERP_BILINEAR)
324     else
325         img.pixbuf = $rotated_pixbufs[thumbnail_img][:pixbuf] = pixbuf
326     end
327 end
328
329 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
330     $modified = true
331
332     rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
333
334     save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
335               Proc.new { |angle|
336                   rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
337                   $notebook.set_page(attributes_prefix != '' ? 0 : 1)
338                   Proc.new {
339                       rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
340                       $notebook.set_page(0)
341                       $notebook.set_page(attributes_prefix != '' ? 0 : 1)
342                   }
343               }, -angle)
344 end
345
346 def color_swap(xmldir, attributes_prefix)
347     $modified = true
348     if xmldir.attributes["#{attributes_prefix}color-swap"]
349         xmldir.delete_attribute("#{attributes_prefix}color-swap")
350     else
351         xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
352     end
353 end
354
355 def enhance(xmldir, attributes_prefix)
356     $modified = true
357     if xmldir.attributes["#{attributes_prefix}enhance"]
358         xmldir.delete_attribute("#{attributes_prefix}enhance")
359     else
360         xmldir.add_attribute("#{attributes_prefix}enhance", '1')
361     end
362 end
363
364 def change_frame_offset(xmldir, attributes_prefix, value)
365     $modified = true
366     xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
367 end
368
369 def ask_new_frame_offset(xmldir, attributes_prefix)
370     value = xmldir.attributes["#{attributes_prefix}frame-offset"]
371
372     dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
373                              $main_window,
374                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
375                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
376                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
377
378     lbl = Gtk::Label.new
379     lbl.markup = utf8(
380 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
381 from. There are approximately 25 frames per second in a video.
382 "))
383     dialog.vbox.add(lbl)
384     dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
385     entry.signal_connect('key-press-event') { |w, event|
386         if event.keyval == Gdk::Keyval::GDK_Return
387             dialog.response(Gtk::Dialog::RESPONSE_OK)
388             true
389         elsif event.keyval == Gdk::Keyval::GDK_Escape
390             dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
391             true
392         else
393             false  #- propagate if needed
394         end
395     }
396     
397     dialog.window_position = Gtk::Window::POS_MOUSE
398     dialog.show_all
399
400     dialog.run { |response|
401         $modified = true
402         newval = entry.text
403         dialog.destroy
404         if response == Gtk::Dialog::RESPONSE_OK
405             msg 3, "changing frame offset top #{newval}"
406             return { :old => value, :new => newval }
407         else
408             return nil
409         end
410     }
411 end
412
413 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img)
414     Thread.new {
415         push_mousecursor_wait
416         system("rm -f '#{destfile}'")
417         #- type can be 'element' or 'subdir'
418         if type == 'element'
419             gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
420         else
421             gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], find_subalbum_info_type(xmldir))
422         end
423         img.set(destfile)
424         $rotated_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
425         if entry2type(origfile) == 'video'
426             #- cleanup temp for videos
427             system("rm -f #{current_dest_dir}/screenshot.jpg000000.jpg")
428         end
429         pop_mousecursor
430     }
431 end
432
433 def popup_thumbnail_menu(event, optionals, type, xmldir, attributes_prefix, closures)
434     menu = Gtk::Menu.new
435     if optionals.include?('change_image')
436         menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
437         changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
438         changeimg.signal_connect('activate') { closures[:change].call }
439         menu.append(            Gtk::SeparatorMenuItem.new)
440     end
441     menu.append(    r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
442     r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
443     r90.signal_connect('activate') { closures[:rotate].call(90) }
444     menu.append(   r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
445     r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
446     r270.signal_connect('activate') { closures[:rotate].call(-90) }
447     if type == 'video'
448         menu.append(               Gtk::SeparatorMenuItem.new)
449         menu.append(  color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
450         color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
451         color_swap.signal_connect('activate') { closures[:color_swap].call }
452         menu.append(        flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
453         flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
454         flip.signal_connect('activate') { closures[:rotate].call(180) }
455         menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
456         frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
457         frame_offset.signal_connect('activate') { closures[:frame_offset].call }
458     end
459     menu.append(               Gtk::SeparatorMenuItem.new)
460     menu.append(enhance      = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
461                                                                                                               _("Enhance constrast"))))
462     enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
463     enhance.signal_connect('activate') { closures[:enhance].call }
464     if optionals.include?('delete')
465         menu.append(               Gtk::SeparatorMenuItem.new)
466         menu.append(delete_item  = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
467         delete_item.signal_connect('activate') { closures[:delete].call }
468     end
469     menu.show_all
470     menu.popup(nil, nil, event.button, event.time)
471 end
472
473 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
474
475     img = nil
476     frame1 = Gtk::Frame.new
477
478     my_gen_real_thumbnail = proc {
479         gen_real_thumbnail('element', from_utf8("#{$current_path}/#{filename}"), thumbnail_img, $xmldir, $default_size['thumbnails'], img)
480     }
481
482     #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
483     if !$rotated_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
484         frame1.add(img = Gtk::Image.new)
485         my_gen_real_thumbnail.call
486     else
487         frame1.add(img = Gtk::Image.new($rotated_pixbufs[thumbnail_img] ? $rotated_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
488     end
489     evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
490
491     tooltips = Gtk::Tooltips.new
492     tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
493     tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(from_utf8("#{$current_path}/#{filename}"))/1024)]) : tipname), nil)
494
495     frame2, textview = create_editzone($autotable_sw, 1, img)
496     textview.buffer.text = utf8(caption)
497     textview.set_justification(Gtk::Justification::CENTER)
498
499     vbox = Gtk::VBox.new(false, 5)
500     vbox.pack_start(evtbox, false, false)
501     vbox.pack_start(frame2, false, false)
502     autotable.append(vbox, filename)
503
504     #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
505     $vbox2textview[vbox] = textview
506
507     #- to be able to find widgets by name
508     $name2widgets[filename] = { :textview => textview }
509
510     cleanup_all_thumbnails = Proc.new {
511         #- remove out of sync images
512         dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
513         for sizeobj in $images_size
514             system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
515         end
516
517     }
518
519     rotate_and_cleanup = Proc.new { |angle|
520         rotate(angle, thumbnail_img, img, $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
521         cleanup_all_thumbnails.call
522     }
523
524     color_swap_and_cleanup = Proc.new {
525         perform_color_swap_and_cleanup = Proc.new {
526             color_swap($xmldir.elements["[@filename='#{filename}']"], '')
527             my_gen_real_thumbnail.call
528         }
529
530         cleanup_all_thumbnails.call
531         perform_color_swap_and_cleanup.call
532
533         save_undo(_("color swap"),
534                   Proc.new {
535                       perform_color_swap_and_cleanup.call
536                       $notebook.set_page(1)
537                       Proc.new {
538                           perform_color_swap_and_cleanup.call
539                           $notebook.set_page(1)
540                       }
541                   })
542     }
543
544     change_frame_offset_and_cleanup = Proc.new {
545         if values = ask_new_frame_offset($xmldir.elements["[@filename='#{filename}']"], '')
546             perform_change_frame_offset_and_cleanup = Proc.new { |val|
547                 change_frame_offset($xmldir.elements["[@filename='#{filename}']"], '', val)
548                 my_gen_real_thumbnail.call
549             }
550             perform_change_frame_offset_and_cleanup.call(values[:new])
551
552             save_undo(_("specify frame offset"),
553                       Proc.new {
554                           perform_change_frame_offset_and_cleanup.call(values[:old])
555                           $notebook.set_page(1)
556                           Proc.new {
557                               perform_change_frame_offset_and_cleanup.call(values[:new])
558                               $notebook.set_page(1)
559                           }
560                       })
561         end
562     }
563
564     enhance_and_cleanup = Proc.new {
565         perform_enhance_and_cleanup = Proc.new {
566             enhance($xmldir.elements["[@filename='#{filename}']"], '')
567             my_gen_real_thumbnail.call
568         }
569
570         cleanup_all_thumbnails.call
571         perform_enhance_and_cleanup.call
572
573         save_undo(_("enhance"),
574                   Proc.new {
575                       perform_enhance_and_cleanup.call
576                       $notebook.set_page(1)
577                       Proc.new {
578                           perform_enhance_and_cleanup.call
579                           $notebook.set_page(1)
580                       }
581                   })
582     }
583
584     delete = Proc.new {
585         if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed?")), { :okcancel => true })
586             $modified = true
587             after = nil
588             perform_delete = Proc.new {
589                 after = autotable.get_next_widget(vbox)
590                 if !after
591                     after = autotable.get_previous_widget(vbox)
592                 end
593                 autotable.remove(vbox)
594                 if after
595                     $vbox2textview[after].grab_focus
596                 end
597             }
598             
599             perform_delete.call
600
601             if !after
602                 if $xmldir.elements['dir']
603                     $xmldir.delete_attribute('thumbnails-caption')
604                     $xmldir.delete_attribute('thumbnails-captionfile')
605                 else
606                     $xmldir.remove
607                 end
608                 save_changes('forced')
609                 populate_subalbums_treeview
610             else
611                 save_undo(_("delete"),
612                           Proc.new { |pos|
613                               autotable.reinsert(pos, vbox, filename)
614                               $notebook.set_page(1)
615                               Proc.new {
616                                   perform_delete.call
617                                   $notebook.set_page(1)
618                               }
619                           }, autotable.get_current_number(vbox))
620             end
621         end
622     }
623
624     textview.signal_connect('key-press-event') { |w, event|
625         propagate = true
626         if event.state != 0
627             x, y = autotable.get_current_pos(vbox)
628             control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
629             shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
630             alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
631             if event.keyval == Gdk::Keyval::GDK_Up && y > 0
632                 if control_pressed
633                     $vbox2textview[autotable.get_widget_at_pos(x, y - 1)].grab_focus
634                 end
635                 if shift_pressed
636                     autotable.move_up(vbox)
637                     textview.grab_focus  #- because if moving, focus is stolen
638                 end
639             end
640             if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
641                 if control_pressed
642                     $vbox2textview[autotable.get_widget_at_pos(x, y + 1)].grab_focus
643                 end
644                 if shift_pressed
645                     autotable.move_down(vbox)
646                     textview.grab_focus  #- because if moving, focus is stolen
647                 end
648             end
649             if event.keyval == Gdk::Keyval::GDK_Left
650                 previous = autotable.get_previous_widget(vbox)
651                 if previous && autotable.get_current_pos(previous)[0] < x
652                     if control_pressed
653                         $vbox2textview[previous].grab_focus
654                     end
655                     if shift_pressed
656                         autotable.move_left(vbox)
657                         textview.grab_focus  #- because if moving, focus is stolen
658                     end
659                 end
660                 if alt_pressed
661                     rotate_and_cleanup.call(-90)
662                 end
663             end
664             if event.keyval == Gdk::Keyval::GDK_Right
665                 next_ = autotable.get_next_widget(vbox)
666                 if next_ && autotable.get_current_pos(next_)[0] > x
667                     if control_pressed
668                         $vbox2textview[next_].grab_focus
669                     end
670                     if shift_pressed
671                         autotable.move_right(vbox)
672                         textview.grab_focus  #- because if moving, focus is stolen
673                     end
674                 end
675                 if alt_pressed
676                     rotate_and_cleanup.call(90)
677                 end
678             end
679             if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
680                 delete.call
681             end
682             if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
683                 view_element(filename)
684                 propagate = false
685             end
686         end
687         !propagate  #- propagate if needed
688     }
689
690     evtbox.signal_connect('button-press-event') { |w, event|
691         retval = true
692         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
693             shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
694             if $r90.active?
695                 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
696             elsif $r270.active?
697                 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
698             elsif $enhance.active?
699                 enhance_and_cleanup.call
700             elsif $delete.active?
701                 delete.call
702             else
703                 textview.grab_focus
704             end
705         end
706         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
707             popup_thumbnail_menu(event, ['delete'], type, $xmldir.elements["[@filename='#{filename}']"], '',
708                                  { :rotate => rotate_and_cleanup, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
709                                    :frame_offset => change_frame_offset_and_cleanup, :delete => delete })
710         end
711         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
712             view_element(filename)
713         else
714             retval = false  #- propagate
715         end
716         retval
717     }
718
719     vbox.signal_connect('button-press-event') { |w, event|
720         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
721             $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
722         end
723         false
724     }
725     vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
726         if $gesture_press && $gesture_press[:filename] == filename
727             if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
728                 angle = x-$gesture_press[:x] > 0 ? 90 : -90
729                 msg 3, "gesture rotate: #{angle}"
730                 rotate_and_cleanup.call(angle)
731             end
732         end
733         $gesture_press = nil
734     }
735
736     #- handle reordering with drag and drop
737     Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
738     Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
739     vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
740         selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
741     }
742     vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
743         ctxt.targets.each { |target|
744             if target.name == 'reorder-elements'
745                 from, to = selection_data.data.to_i, autotable.get_current_number(vbox)
746                 if from != to
747                     $modified = true
748                     autotable.move(from, to)
749                     save_undo(_("reorder"),
750                               Proc.new { |from, to|
751                                   if to > from
752                                       autotable.move(to - 1, from)
753                                   else
754                                       autotable.move(to, from + 1)
755                                   end
756                                   $notebook.set_page(1)
757                                   Proc.new {
758                                       autotable.move(from, to)
759                                       $notebook.set_page(1)
760                                   }
761                               }, from, to)
762                 end
763             end
764         }
765     }
766
767     vbox.show_all
768 end
769
770 def create_auto_table
771
772     $autotable = Gtk::AutoTable.new(5)
773
774     $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
775     $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
776     $autotable_sw.add_with_viewport($autotable)
777 end
778
779 def create_subalbums_page
780
781     subalbums_hb = Gtk::HBox.new
782     $subalbums_vb = Gtk::VBox.new(false, 5)
783     subalbums_hb.pack_start($subalbums_vb, false, false)
784     $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
785     $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
786     $subalbums_sw.add_with_viewport(subalbums_hb)
787 end
788
789 def save_current_file
790     save_changes
791     if $filename
792         ios = File.open($filename, "w")
793         $xmldoc.write(ios, 0)
794         ios.close
795         $modified = false
796     end
797 end
798
799 def try_quit
800     if $modified
801         dialog = Gtk::Dialog.new(utf8(_("Save before quitting?")),
802                                  $main_window,
803                                  Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
804                                  [Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
805                                  [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
806                                  [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
807         dialog.vbox.add(Gtk::Label.new(utf8(_("Do you want to save your changes before quitting?"))))
808         dialog.window_position = Gtk::Window::POS_CENTER
809         dialog.show_all
810         
811         dialog.run { |response|
812             dialog.destroy
813             if response == Gtk::Dialog::RESPONSE_CANCEL
814                 return
815             elsif response == Gtk::Dialog::RESPONSE_YES
816                 save_current_file
817             end
818         }
819     end
820
821     Gtk.main_quit
822 end
823
824 def show_popup(parent, msg, *options)
825     dialog = Gtk::Dialog.new
826     dialog.title = utf8(_("Booh message"))
827     lbl = Gtk::Label.new
828     lbl.markup = msg
829     if options[0] && options[0][:centered]
830         lbl.set_justify(Gtk::Justification::CENTER)
831     end
832     dialog.vbox.add(lbl)
833     if options[0] && options[0][:okcancel]
834         dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
835     end
836     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
837
838     dialog.transient_for = parent
839     dialog.set_default_size(200, 120)
840     if options[0] && options[0][:pos_centered]
841         dialog.window_position = Gtk::Window::POS_CENTER
842     else
843         dialog.window_position = Gtk::Window::POS_MOUSE
844     end
845     dialog.show_all
846
847     dialog.run { |response|
848         dialog.destroy
849         if options[0] && options[0][:okcancel]
850             return response == Gtk::Dialog::RESPONSE_OK
851         end
852     }
853 end
854
855 def wait_message(parent, msg)
856     w = Gtk::Window.new
857     w.set_transient_for(parent)
858     w.modal = true
859
860     vb = Gtk::VBox.new(false, 5)
861     vb.set_border_width(5)
862     vb.pack_start(Gtk::Label.new(msg), false, false)
863     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
864     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
865     vb.pack_start(pb = Gtk::ProgressBar.new.set_pulse_step(0.05), false, false)
866     vb.pack_start(Gtk::HSeparator.new, false, false)
867     vb.pack_end(bottom, false, false)
868
869     timeout = Gtk.timeout_add(200) { pb.pulse }
870
871     w.add(vb)
872     w.signal_connect('delete-event') { w.destroy }
873     w.signal_connect('destroy') { Gtk.timeout_remove(timeout) }
874     w.window_position = Gtk::Window::POS_CENTER
875     w.show_all
876
877     return [ b, w ]
878 end
879
880 def perform_in_background(cmd, waitmsg, params)
881     button, w8 = wait_message($main_window, waitmsg)
882     pid = nil
883     Thread.new {
884         msg 2, cmd
885         if pid = fork
886             id, exitstatus = Process.waitpid2(pid)
887             w8.destroy
888             if exitstatus == 0
889                 if params[:successmsg]
890                     show_popup($main_window, params[:successmsg])
891                 end
892                 if params[:closure_after]
893                     params[:closure_after].call
894                 end
895             elsif exitstatus == 15
896                 #- say nothing, user aborted
897             else
898                 if params[:failuremsg]
899                     show_popup($main_window, params[:failuremsg])
900                 end
901             end
902         else
903             exec(cmd)
904         end
905     }
906     button.signal_connect('clicked') {
907         Process.kill('SIGTERM', pid)
908     }
909 end
910
911 def save_changes(*forced)
912     if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
913         return
914     end
915
916     if $xmldir.elements['dir']
917         $xmldir.add_attribute('subdirs-caption', $subalbums_title.buffer.text)
918         $xmldir.elements.each('dir') { |element|
919             path = element.attributes['path']
920             if element.attributes['subdirs-caption']
921                 element.add_attribute('subdirs-caption',     $subalbums_edits[path][:editzone].buffer.text)
922                 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
923             else
924                 element.add_attribute('thumbnails-caption',     $subalbums_edits[path][:editzone].buffer.text)
925                 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
926             end
927         }
928         if $xmldir.attributes['thumbnails-caption']
929             path = $xmldir.attributes['path']
930             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
931         end
932     end
933
934     #- remove and reinsert elements to reflect new ordering
935     save_attributes = {}
936     save_types = {}
937     cpt = 0
938     $xmldir.elements.each { |element|
939         if element.name == 'image' || element.name == 'video'
940             save_types[element.attributes['filename']] = element.name
941             save_attributes[element.attributes['filename']] = element.attributes
942             element.remove
943             cpt += 1
944         end
945     }
946     $autotable.current_order.each { |path|
947         chld = $xmldir.add_element save_types[path], save_attributes[path]
948         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
949     }
950 end
951
952 def change_dir
953     $autotable.clear
954     $vbox2textview = {}
955     $name2widgets = {}
956     UndoHandler.cleanup
957     $undo_tb.sensitive = $undo_mb.sensitive = false
958     $redo_tb.sensitive = $redo_mb.sensitive = false
959
960     if !$current_path
961         return
962     end
963
964     $subalbums_vb.children.each { |chld|
965         $subalbums_vb.remove(chld)
966     }
967     $subalbums = Gtk::Table.new(0, 0, true)
968     current_y_sub_albums = 0
969
970     $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
971     $subalbums_edits = {}
972
973     add_subalbum = Proc.new { |xmldir|
974         if xmldir == $xmldir
975             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
976             caption = xmldir.attributes['thumbnails-caption']
977             captionfile, dummy = find_subalbum_caption_info(xmldir)
978             infotype = 'thumbnails'
979         else
980             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
981             captionfile, caption = find_subalbum_caption_info(xmldir)
982             infotype = find_subalbum_info_type(xmldir)
983         end
984         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
985         hbox = Gtk::HBox.new
986         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
987         f = Gtk::Frame.new
988         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
989
990         img = nil
991         my_gen_real_thumbnail = proc {
992             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img)
993         }
994
995         if !$rotated_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
996             f.add(img = Gtk::Image.new)
997             my_gen_real_thumbnail.call
998         else
999             f.add(img = Gtk::Image.new($rotated_pixbufs[thumbnail_file] ? $rotated_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
1000         end
1001         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
1002         $subalbums.attach(hbox,
1003                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1004
1005         change_image = Proc.new {
1006             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
1007                                             nil,
1008                                             Gtk::FileChooser::ACTION_OPEN,
1009                                             nil,
1010                                             [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1011             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
1012             fc.transient_for = $main_window
1013             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))
1014             f.add(preview_img = Gtk::Image.new)
1015             preview.show_all
1016             fc.signal_connect('update-preview') { |w|
1017                 begin
1018                     if fc.preview_filename
1019                         preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
1020                         fc.preview_widget_active = true
1021                     end
1022                 rescue Gdk::PixbufError
1023                     fc.preview_widget_active = false
1024                 end
1025             }
1026             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1027                 $modified = true
1028                 old_file = captionfile
1029                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
1030                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
1031                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
1032                 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
1033
1034                 new_file = fc.filename
1035                 msg 3, "new captionfile is: #{fc.filename}"
1036                 perform_changefile = Proc.new {
1037                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
1038                     $rotated_pixbufs.delete(thumbnail_file)
1039                     xmldir.delete_attribute("#{infotype}-rotate")
1040                     xmldir.delete_attribute("#{infotype}-color-swap")
1041                     xmldir.delete_attribute("#{infotype}-enhance")
1042                     xmldir.delete_attribute("#{infotype}-frame-offset")
1043                     my_gen_real_thumbnail.call
1044                 }
1045                 perform_changefile.call
1046
1047                 save_undo(_("change caption file for sub-album"),
1048                           Proc.new {
1049                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
1050                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
1051                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
1052                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
1053                               xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
1054                               my_gen_real_thumbnail.call
1055                               $notebook.set_page(0)
1056                               Proc.new {
1057                                   perform_changefile.call
1058                                   $notebook.set_page(0)
1059                               }
1060                           })
1061             end
1062             fc.destroy
1063         }
1064
1065         rotate_and_cleanup = Proc.new { |angle|
1066             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
1067             system("rm -f '#{thumbnail_file}'")
1068         }
1069
1070         color_swap_and_cleanup = Proc.new {
1071             perform_color_swap_and_cleanup = Proc.new {
1072                 color_swap(xmldir, "#{infotype}-")
1073                 my_gen_real_thumbnail.call
1074             }
1075             perform_color_swap_and_cleanup.call
1076
1077             save_undo(_("color swap"),
1078                       Proc.new {
1079                           perform_color_swap_and_cleanup.call
1080                           $notebook.set_page(0)
1081                           Proc.new {
1082                               perform_color_swap_and_cleanup.call
1083                               $notebook.set_page(0)
1084                           }
1085                       })
1086         }
1087
1088         change_frame_offset_and_cleanup = Proc.new {
1089             if values = ask_new_frame_offset(xmldir, "#{infotype}-")
1090                 perform_change_frame_offset_and_cleanup = Proc.new { |val|
1091                     change_frame_offset(xmldir, "#{infotype}-", val)
1092                     my_gen_real_thumbnail.call
1093                 }
1094                 perform_change_frame_offset_and_cleanup.call(values[:new])
1095
1096                 save_undo(_("specify frame offset"),
1097                           Proc.new {
1098                               perform_change_frame_offset_and_cleanup.call(values[:old])
1099                               $notebook.set_page(0)
1100                               Proc.new {
1101                                   perform_change_frame_offset_and_cleanup.call(values[:new])
1102                                   $notebook.set_page(0)
1103                               }
1104                           })
1105             end
1106         }
1107
1108         enhance_and_cleanup = Proc.new {
1109             perform_enhance_and_cleanup = Proc.new {
1110                 enhance(xmldir, "#{infotype}-")
1111                 my_gen_real_thumbnail.call
1112             }
1113             
1114             perform_enhance_and_cleanup.call
1115             
1116             save_undo(_("enhance"),
1117                       Proc.new {
1118                           perform_enhance_and_cleanup.call
1119                           $notebook.set_page(0)
1120                           Proc.new {
1121                               perform_enhance_and_cleanup.call
1122                               $notebook.set_page(0)
1123                           }
1124                       })
1125         }
1126
1127         evtbox.signal_connect('button-press-event') { |w, event|
1128             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1129                 if $r90.active?
1130                     rotate_and_cleanup.call(90)
1131                 elsif $r270.active?
1132                     rotate_and_cleanup.call(-90)
1133                 elsif $enhance.active?
1134                     enhance_and_cleanup.call
1135                 end
1136             end
1137             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1138                 popup_thumbnail_menu(event, ['change_image'], entry2type(captionfile), xmldir, "#{infotype}-",
1139                                      { :change => change_image, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
1140                                        :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup })
1141             end
1142             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1143                 change_image.call
1144                 true   #- handled
1145             end
1146         }
1147         evtbox.signal_connect('button-press-event') { |w, event|
1148             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
1149             false
1150         }
1151
1152         evtbox.signal_connect('button-release-event') { |w, event|
1153             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
1154                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
1155                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
1156                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
1157                     msg 3, "gesture rotate: #{angle}"
1158                     rotate_and_cleanup.call(angle)
1159                 end
1160             end
1161             $gesture_press = nil
1162         }
1163         
1164         frame, textview = create_editzone($subalbums_sw, 0, img)
1165         textview.buffer.text = caption
1166         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
1167                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1168         
1169         $subalbums_edits[xmldir.attributes['path']] = { :editzone => textview, :captionfile => captionfile }
1170         current_y_sub_albums += 1
1171     }
1172
1173     if $xmldir.elements['dir']
1174         #- title edition
1175         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
1176         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
1177         $subalbums_title.set_justification(Gtk::Justification::CENTER)
1178         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1179         #- this album image/caption
1180         if $xmldir.attributes['thumbnails-caption']
1181             add_subalbum.call($xmldir)
1182         end
1183     end
1184     $xmldir.elements.each { |element|
1185         if element.name == 'image' || element.name == 'video'
1186             #- element (image or video) of this album
1187             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
1188             msg 3, "dest_img: #{dest_img}"
1189             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
1190         end
1191         if element.name == 'dir'
1192             #- sub-album image/caption
1193             add_subalbum.call(element)
1194         end
1195     }
1196     $subalbums_vb.add($subalbums)
1197     $subalbums_vb.show_all
1198
1199     if !$xmldir.elements['image'] && !$xmldir.elements['video']
1200         $notebook.get_tab_label($autotable_sw).sensitive = false
1201         $notebook.set_page(0)
1202     else
1203         $notebook.get_tab_label($autotable_sw).sensitive = true
1204     end
1205
1206     if !$xmldir.elements['dir']
1207         $notebook.get_tab_label($subalbums_sw).sensitive = false
1208         $notebook.set_page(1)
1209     else
1210         $notebook.get_tab_label($subalbums_sw).sensitive = true
1211     end
1212 end
1213
1214 def pixbuf_or_nil(filename)
1215     begin
1216         return Gdk::Pixbuf.new(filename)
1217     rescue
1218         return nil
1219     end
1220 end
1221
1222 def theme_choose(current)
1223     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
1224                              $main_window,
1225                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1226                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1227                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1228
1229     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
1230     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
1231     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
1232     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
1233     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
1234     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
1235     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
1236     treeview.signal_connect('button-press-event') { |w, event|
1237         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1238             dialog.response(Gtk::Dialog::RESPONSE_OK)
1239         end
1240     }
1241
1242     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
1243
1244     `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
1245         dir.chomp!
1246         iter = model.append
1247         iter[0] = File.basename(dir)
1248         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
1249         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
1250         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
1251         if File.basename(dir) == current
1252             treeview.selection.select_iter(iter)
1253         end
1254     }
1255
1256     dialog.set_default_size(700, 400)
1257     dialog.vbox.show_all
1258     dialog.run { |response|
1259         iter = treeview.selection.selected
1260         dialog.destroy
1261         if response == Gtk::Dialog::RESPONSE_OK && iter
1262             return model.get_value(iter, 0)
1263         end
1264     }
1265     return nil
1266 end
1267
1268 def populate_subalbums_treeview
1269     $albums_ts.clear
1270     $autotable.clear
1271     $subalbums_vb.children.each { |chld|
1272         $subalbums_vb.remove(chld)
1273     }
1274
1275     source = $xmldoc.root.attributes['source']
1276     msg 3, "source: #{source}"
1277
1278     xmldir = $xmldoc.elements["//dir[@path='#{source}']"]
1279     if !xmldir
1280         msg 1, _("Corrupted booh file...")
1281         return
1282     end
1283
1284     append_dir_elem = Proc.new { |parent_iter, location|
1285         child_iter = $albums_ts.append(parent_iter)
1286         child_iter[0] = File.basename(location)
1287         child_iter[1] = location
1288         msg 3, "puttin location: #{location}"
1289         $xmldoc.elements.each("//dir[@path='#{location}']/dir") { |elem|
1290             append_dir_elem.call(child_iter, elem.attributes['path'])
1291         }
1292     }
1293     append_dir_elem.call(nil, source)
1294
1295     $albums_tv.expand_all
1296     $albums_tv.selection.select_iter($albums_ts.iter_first)
1297 end
1298
1299 def open_file(filename)
1300
1301     $filename = nil
1302     $modified = false
1303     $current_path = nil   #- invalidate
1304     $rotated_pixbufs = {}
1305     $albums_ts.clear
1306     $autotable.clear
1307     $subalbums_vb.children.each { |chld|
1308         $subalbums_vb.remove(chld)
1309     }
1310
1311     if !File.exists?(filename)
1312         return utf8(_("File not found."))
1313     end
1314
1315     begin
1316         $xmldoc = REXML::Document.new File.new(filename)
1317     rescue Exception
1318         $xmldoc = nil
1319     end
1320
1321     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
1322         if entry2type(filename).nil?
1323             return utf8(_("Not a booh file!"))
1324         else
1325             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."))
1326         end
1327     end
1328
1329     if !source = $xmldoc.root.attributes['source']
1330         return utf8(_("Corrupted booh file..."))
1331     end
1332
1333     if !dest = $xmldoc.root.attributes['destination']
1334         return utf8(_("Corrupted booh file..."))
1335     end
1336
1337     if !theme = $xmldoc.root.attributes['theme']
1338         return utf8(_("Corrupted booh file..."))
1339     end
1340
1341     limit_sizes = $xmldoc.root.attributes['limit-sizes']
1342
1343     $filename = filename
1344     select_theme(theme, limit_sizes)
1345     $default_size['thumbnails'] =~ /(.*)x(.*)/
1346     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
1347     $albums_thumbnail_size =~ /(.*)x(.*)/
1348     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
1349
1350     populate_subalbums_treeview
1351
1352     $config['last-opens'] ||= []
1353     if $config['last-opens'][-1] != utf8(filename)
1354         $config['last-opens'] << utf8(filename)
1355     end
1356     $save.sensitive = $save_as.sensitive = $merge.sensitive = $merge_subalbums.sensitive = $generate.sensitive = $properties.sensitive = true
1357     return nil
1358 end
1359
1360 def open_file_popup
1361     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
1362                                     nil,
1363                                     Gtk::FileChooser::ACTION_OPEN,
1364                                     nil,
1365                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1366     fc.add_shortcut_folder(File.expand_path("~/.booh"))
1367     fc.set_current_folder(File.expand_path("~/.booh"))
1368     fc.transient_for = $main_window
1369     ok = false
1370     while !ok
1371         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1372             push_mousecursor_wait(fc)
1373             msg = open_file(fc.filename)
1374             pop_mousecursor(fc)
1375             if msg
1376                 show_popup(fc, msg)
1377                 ok = false
1378             else
1379                 ok = true
1380             end
1381         else
1382             ok = true
1383         end
1384     end
1385     fc.destroy
1386 end
1387
1388 def additional_booh_options
1389     options = ''
1390     if $config['mproc']
1391         options += "--mproc #{$config['mproc'].to_i} "
1392     end
1393     return options
1394 end
1395
1396 def new_album
1397     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
1398                              $main_window,
1399                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1400                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1401                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1402     
1403     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
1404     tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
1405                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1406     tbl.attach(src = Gtk::Entry.new,
1407                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1408     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
1409                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1410     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
1411                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1412     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
1413                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
1414     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
1415                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1416     tbl.attach(dest = Gtk::Entry.new,
1417                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
1418     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
1419                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1420     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
1421                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1422     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
1423                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
1424     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
1425                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1426
1427     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(tbl = Gtk::Table.new(0, 0, false))
1428     tbl.attach(Gtk::Label.new(utf8(_("Theme: "))),
1429                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1430     tbl.attach(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'),
1431                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1432     tbl.attach(Gtk::Label.new(utf8(_("Sizes of images to generate: "))),
1433                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1434     tbl.attach(sizes = Gtk::HBox.new,
1435                1, 3, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1436
1437     src_nb_calculated_for = ''
1438     src_nb_thread = nil
1439     process_src_nb = Proc.new {
1440         if src.text != src_nb_calculated_for
1441             src_nb_calculated_for = src.text
1442             if src_nb_thread
1443                 Thread.kill(src_nb_thread)
1444                 src_nb_thread = nil
1445             end
1446             if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
1447                 if File.readable?(from_utf8(src_nb_calculated_for))
1448                     src_nb_thread = Thread.new {
1449                         src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
1450                         total = { 'image' => 0, 'video' => 0, nil => 0 }
1451                         `find '#{from_utf8(src_nb_calculated_for)}' -type d`.each { |dir|
1452                             if File.basename(dir) =~ /^\./
1453                                 next
1454                             else
1455                                 begin
1456                                     Dir.entries(dir.chomp).each { |file|
1457                                         total[entry2type(file)] += 1
1458                                     }
1459                                 rescue Errno::EACCES, Errno::ENOENT
1460                                 end
1461                             end
1462                         }
1463                         src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ]))
1464                         src_nb_thread = nil
1465                     }
1466                 else
1467                     src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
1468                 end
1469             else
1470                 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
1471             end
1472         end
1473         true
1474     }
1475     timeout_src_nb = Gtk.timeout_add(100) {
1476         process_src_nb.call
1477     }
1478
1479     src_browse.signal_connect('clicked') {
1480         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
1481                                         nil,
1482                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
1483                                         nil,
1484                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1485         fc.transient_for = $main_window
1486         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1487             src.text = utf8(fc.filename)
1488             process_src_nb.call
1489             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
1490         end
1491         fc.destroy
1492     }
1493
1494     dest_browse.signal_connect('clicked') {
1495         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
1496                                         nil,
1497                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
1498                                         nil,
1499                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1500         fc.transient_for = $main_window
1501         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1502             dest.text = utf8(fc.filename)
1503         end
1504         fc.destroy
1505     }
1506
1507     conf_browse.signal_connect('clicked') {
1508         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
1509                                         nil,
1510                                         Gtk::FileChooser::ACTION_SAVE,
1511                                         nil,
1512                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1513         fc.transient_for = $main_window
1514         fc.add_shortcut_folder(File.expand_path("~/.booh"))
1515         fc.set_current_folder(File.expand_path("~/.booh"))
1516         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1517             conf.text = utf8(fc.filename)
1518         end
1519         fc.destroy
1520     }
1521
1522     theme_sizes = []
1523     recreate_theme_config = proc {
1524         theme_sizes.each { |e| sizes.remove(e[:widget]) }
1525         theme_sizes = []
1526         select_theme(theme_button.label, 'all')
1527         $images_size.each { |s|
1528             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
1529             if !s['optional']
1530                 cb.active = true
1531             end
1532             tooltips = Gtk::Tooltips.new
1533             tooltips.set_tip(cb, utf8(s['description']), nil)
1534             theme_sizes << { :widget => cb, :value => s['name'] }
1535         }
1536         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
1537         tooltips = Gtk::Tooltips.new
1538         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
1539         theme_sizes << { :widget => cb, :value => 'original' }
1540         sizes.show_all
1541     }
1542     recreate_theme_config.call
1543
1544     theme_button.signal_connect('clicked') {
1545         if newtheme = theme_choose(theme_button.label)
1546             theme_button.label = newtheme
1547             recreate_theme_config.call
1548         end
1549     }
1550
1551     dialog.vbox.add(frame1)
1552     dialog.vbox.add(frame2)
1553     dialog.window_position = Gtk::Window::POS_MOUSE
1554     dialog.show_all
1555
1556     keepon = true
1557     ok = true
1558     while keepon
1559         dialog.run { |response|
1560             if response == Gtk::Dialog::RESPONSE_OK
1561                 srcdir = from_utf8(src.text)
1562                 destdir = from_utf8(dest.text)
1563                 if !File.directory?(srcdir)
1564                     show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
1565                     src.grab_focus
1566                 elsif conf.text == ''
1567                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
1568                     conf.grab_focus
1569                 elsif destdir != make_dest_filename(destdir)
1570                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
1571                     dest.grab_focus
1572                 elsif File.directory?(destdir)
1573                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
1574                     dest.grab_focus
1575                 elsif File.exists?(destdir)
1576                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
1577                     dest.grab_focus
1578                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
1579                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
1580                 else
1581                     system("mkdir '#{destdir}'")
1582                     if !File.directory?(destdir)
1583                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
1584                         dest.grab_focus
1585                     else
1586                         keepon = false
1587                     end
1588                 end
1589             else
1590                 keepon = ok = false
1591             end
1592         }
1593     end
1594     srcdir = from_utf8(src.text)
1595     destdir = from_utf8(dest.text)
1596     configskel = File.expand_path(from_utf8(conf.text))
1597     theme = theme_button.label
1598     sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
1599     dialog.destroy
1600     if src_nb_thread
1601         Thread.kill(src_nb_thread)
1602     end
1603     Gtk.timeout_remove(timeout_src_nb)
1604
1605     if ok
1606         perform_in_background("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
1607                                   "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} #{additional_booh_options}",
1608                               utf8(_("Please wait while scanning source directory...")),
1609                               { :closure_after => proc { open_file(configskel) } })
1610     end
1611 end
1612
1613 def properties
1614     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
1615                              $main_window,
1616                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1617                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1618                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1619     
1620     source = $xmldoc.root.attributes['source']
1621     dest = $xmldoc.root.attributes['destination']
1622     theme = $xmldoc.root.attributes['theme']
1623     limit_sizes = $xmldoc.root.attributes['limit-sizes']
1624     if limit_sizes
1625         limit_sizes = limit_sizes.split(/,/)
1626     end
1627
1628     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
1629     tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
1630                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1631     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
1632                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1633     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
1634                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1635     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>')),
1636                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
1637     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
1638                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1639     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $filename + '</i>')),
1640                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
1641
1642     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(tbl = Gtk::Table.new(0, 0, false))
1643     tbl.attach(Gtk::Label.new(utf8(_("Theme: "))),
1644                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1645     tbl.attach(theme_button = Gtk::Button.new(theme),
1646                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1647     tbl.attach(Gtk::Label.new(utf8(_("Sizes of images to generate: "))),
1648                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1649     tbl.attach(sizes = Gtk::HBox.new,
1650                1, 3, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1651
1652     theme_sizes = []
1653     recreate_theme_config = proc {
1654         theme_sizes.each { |e| sizes.remove(e[:widget]) }
1655         theme_sizes = []
1656         select_theme(theme_button.label, 'all')
1657         $images_size.each { |s|
1658             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
1659             if limit_sizes
1660                 if limit_sizes.include?(s['name'])
1661                     cb.active = true
1662                 end
1663             else
1664                 if !s['optional']
1665                     cb.active = true
1666                 end
1667             end
1668             tooltips = Gtk::Tooltips.new
1669             tooltips.set_tip(cb, utf8(s['description']), nil)
1670             theme_sizes << { :widget => cb, :value => s['name'] }
1671         }
1672         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
1673         tooltips = Gtk::Tooltips.new
1674         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
1675         if limit_sizes && limit_sizes.include?('original')
1676             cb.active = true
1677         end
1678         theme_sizes << { :widget => cb, :value => 'original' }
1679         sizes.show_all
1680     }
1681     recreate_theme_config.call
1682
1683     theme_button.signal_connect('clicked') {
1684         if newtheme = theme_choose(theme_button.label)
1685             limit_sizes = nil
1686             theme_button.label = newtheme
1687             recreate_theme_config.call
1688         end
1689     }
1690
1691     dialog.vbox.add(frame1)
1692     dialog.vbox.add(frame2)
1693     dialog.window_position = Gtk::Window::POS_MOUSE
1694     dialog.show_all
1695
1696     keepon = true
1697     ok = true
1698     while keepon
1699         dialog.run { |response|
1700             if response == Gtk::Dialog::RESPONSE_OK
1701                 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
1702                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
1703                 else
1704                     keepon = false
1705                 end
1706             else
1707                 keepon = ok = false
1708             end
1709         }
1710     end
1711     save_theme = theme_button.label
1712     save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
1713     dialog.destroy
1714
1715     if ok && (save_theme != theme || save_limit_sizes != limit_sizes)
1716         save_current_file
1717         perform_in_background("booh-backend --use-config '#{$filename}' --for-gui " +
1718                                   "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{save_limit_sizes.join(',')} #{additional_booh_options}",
1719                               utf8(_("Please wait while scanning source directory...")),
1720                               { :closure_after => proc { open_file($filename) } })
1721     end
1722 end
1723
1724 def merge
1725     save_current_file
1726
1727     theme = $xmldoc.root.attributes['theme']
1728     limit_sizes = $xmldoc.root.attributes['limit-sizes']
1729     if limit_sizes
1730         limit_sizes = "--sizes #{limit_sizes}"
1731     end
1732     perform_in_background("booh-backend --merge-config '#{$filename}' --for-gui " +
1733                               "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
1734                           utf8(_("Please wait while scanning source directory...")),
1735                           { :closure_after => proc { open_file($filename) } })
1736 end
1737
1738 def merge_subalbums
1739     save_current_file
1740
1741     perform_in_background("booh-backend --merge-config-newdirs '#{$filename}' --for-gui " +
1742                               "--verbose-level #{$verbose_level} #{additional_booh_options}",
1743                           utf8(_("Please wait while scanning source directory...")),
1744                           { :closure_after => proc { open_file($filename) } })
1745 end
1746
1747 def save_as_do
1748     fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
1749                                     nil,
1750                                     Gtk::FileChooser::ACTION_SAVE,
1751                                     nil,
1752                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1753     fc.transient_for = $main_window
1754     fc.add_shortcut_folder(File.expand_path("~/.booh"))
1755     fc.set_current_folder(File.expand_path("~/.booh"))
1756     fc.filename = $filename
1757     if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1758         $filename = fc.filename
1759         save_current_file
1760     end
1761     fc.destroy
1762 end
1763
1764 def preferences
1765     dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
1766                              $main_window,
1767                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1768                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1769                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1770
1771     dialog.vbox.add(notebook = Gtk::Notebook.new)
1772     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
1773     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
1774                0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1775     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
1776                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1777     tooltips = Gtk::Tooltips.new
1778     tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename; for example: mplayer %f")), nil)
1779     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
1780                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
1781     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)),
1782                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
1783     smp_check.signal_connect('toggled') {
1784         if smp_check.active?
1785             smp_hbox.sensitive = true
1786         else
1787             smp_hbox.sensitive = false
1788         end
1789     }
1790     if $config['mproc']
1791         smp_check.active = true
1792         smp_spin.value = $config['mproc'].to_i
1793     end
1794
1795     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
1796     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
1797                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1798     tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
1799                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1800
1801     dialog.vbox.show_all
1802     dialog.run { |response|
1803         if response == Gtk::Dialog::RESPONSE_OK
1804             $config['video-viewer'] = video_viewer_entry.text
1805             if smp_check.active?
1806                 $config['mproc'] = smp_spin.value.to_i
1807             else
1808                 $config.delete('mproc')
1809             end
1810
1811             $config['convert-enhance'] = enhance_entry.text
1812         end
1813     }
1814     dialog.destroy
1815 end
1816
1817 def create_menu_and_toolbar
1818
1819     #- menu
1820     mb = Gtk::MenuBar.new
1821
1822     filemenu = Gtk::MenuItem.new(utf8(_("_File")))
1823     filesubmenu = Gtk::Menu.new
1824     filesubmenu.append(new       = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
1825     filesubmenu.append(open      = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
1826     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
1827     filesubmenu.append($save     = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
1828     filesubmenu.append($save_as  = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
1829     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
1830     tooltips = Gtk::Tooltips.new
1831     filesubmenu.append($merge_subalbums = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums"))).set_sensitive(false))
1832     $merge_subalbums.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
1833     tooltips.set_tip($merge_subalbums, utf8(_("Take into account new/removed subalbums (subdirectories) in the source directory")), nil)
1834     filesubmenu.append($merge    = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums and new/removed images/videos"))).set_sensitive(false))
1835     $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
1836     tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums in the source directory")), nil)
1837     filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
1838     $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
1839     tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
1840     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
1841     filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
1842     tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
1843     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
1844     filesubmenu.append(quit      = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
1845     filemenu.set_submenu(filesubmenu)
1846     mb.append(filemenu)
1847
1848     new.signal_connect('activate') { new_album }
1849     open.signal_connect('activate') { open_file_popup }
1850     $save.signal_connect('activate') { save_current_file }
1851     $save_as.signal_connect('activate') { save_as_do }
1852     $merge.signal_connect('activate') { merge }
1853     $merge_subalbums.signal_connect('activate') { merge_subalbums }
1854     $generate.signal_connect('activate') {
1855         save_current_file
1856         perform_in_background("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
1857                               utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
1858                               { :successmsg => utf8(_("Your web-album is now ready in directory `%s'.") % $xmldoc.root.attributes['destination']),
1859                                 :failuremsg => utf8(_("There was something wrong when generating the web-album, sorry.")) })
1860     }
1861     $properties.signal_connect('activate') { properties }
1862
1863     quit.signal_connect('activate') { try_quit }
1864
1865     editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
1866     editsubmenu = Gtk::Menu.new
1867     editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
1868     editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
1869     editsubmenu.append(           Gtk::SeparatorMenuItem.new)
1870     editsubmenu.append(prefs    = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
1871     editmenu.set_submenu(editsubmenu)
1872     mb.append(editmenu)
1873
1874     prefs.signal_connect('activate') { preferences }
1875
1876     helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
1877     helpsubmenu = Gtk::Menu.new
1878     helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
1879     helpmenu.set_submenu(helpsubmenu)
1880     mb.append(helpmenu)
1881
1882     about.signal_connect('activate') {
1883             show_popup($main_window, utf8(_("<span size='x-large' weight='bold'>Booh %s</span>
1884
1885 <i>``The Web-Album of choice for discriminating Linux users''</i>
1886
1887 Copyright (c) 2005 Guillaume Cottenceau") % $VERSION), { :centered => true, :pos_centered => true })
1888     }
1889
1890
1891     #- toolbar
1892     tb = Gtk::Toolbar.new
1893
1894     tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
1895     open.label = utf8(_("Open"))  #- to avoid missing gtk2 l10n catalogs
1896     open.menu = Gtk::Menu.new
1897     open.signal_connect('clicked') { open_file_popup }
1898     open.signal_connect('show-menu') {
1899         lastopens = Gtk::Menu.new
1900         j = 0
1901         if $config['last-opens']
1902             $config['last-opens'].reverse.each { |e|
1903                 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
1904                 item.signal_connect('activate') {
1905                     push_mousecursor_wait
1906                     msg = open_file(from_utf8(e))
1907                     pop_mousecursor
1908                     if msg
1909                         show_popup($main_window, msg)
1910                     end
1911                 }
1912                 j += 1
1913             }
1914             lastopens.show_all
1915         end
1916         open.menu = lastopens
1917     }
1918
1919     tb.insert(-1, Gtk::SeparatorToolItem.new)
1920
1921     tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
1922     $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
1923     $r90.label = utf8(_("Rotate"))
1924     tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
1925     $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
1926     $r270.label = utf8(_("Rotate"))
1927     tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
1928     $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1929     $enhance.label = utf8(_("Enhance"))
1930     tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
1931     $delete.label = utf8(_("Delete"))  #- to avoid missing gtk2 l10n catalogs
1932     tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
1933     nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
1934     nothing.label = utf8(_("None"))
1935
1936     tb.insert(-1, Gtk::SeparatorToolItem.new)
1937
1938     tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
1939     tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
1940
1941     perform_undo = Proc.new {
1942         $redo_tb.sensitive = $redo_mb.sensitive = true
1943         if not more_undoes = UndoHandler.undo($statusbar)
1944             $undo_tb.sensitive = $undo_mb.sensitive = false
1945         end
1946     }
1947     perform_redo = Proc.new {
1948         $undo_tb.sensitive = $undo_mb.sensitive = true
1949         if not more_redoes = UndoHandler.redo($statusbar)
1950             $redo_tb.sensitive = $redo_mb.sensitive = false
1951         end
1952     }
1953
1954     $undo_tb.signal_connect('clicked')  { perform_undo.call }
1955     $undo_mb.signal_connect('activate') { perform_undo.call }
1956     $redo_tb.signal_connect('clicked')  { perform_redo.call }
1957     $redo_mb.signal_connect('activate') { perform_redo.call }
1958
1959     one_click_explain_try = Proc.new {
1960         if !$config['one-click-explained']
1961             show_popup($main_window, utf8(_("<b>One-Click tools.</b>
1962
1963 You have just clicked on a One-Click tool. When such a tool is activated
1964 (<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
1965 on a thumbnail will immediately apply the desired action.
1966
1967 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
1968 ")))
1969             $config['one-click-explained'] = true
1970         end
1971     }
1972
1973     $r90.signal_connect('toggled') {
1974         if $r90.active?
1975             set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
1976             one_click_explain_try.call
1977             $r270.active = false
1978             $enhance.active = false
1979             $delete.active = false
1980             nothing.sensitive = true
1981         else
1982             if !$r270.active? && !$enhance.active? && !$delete.active?
1983                 set_mousecursor_normal
1984                 nothing.sensitive = false
1985             else
1986                 nothing.sensitive = true
1987             end
1988         end
1989     }
1990     $r270.signal_connect('toggled') {
1991         if $r270.active?
1992             set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
1993             one_click_explain_try.call
1994             $r90.active = false
1995             $enhance.active = false
1996             $delete.active = false
1997             nothing.sensitive = true
1998         else
1999             if !$r90.active? && !$enhance.active? && !$delete.active?
2000                 set_mousecursor_normal
2001                 nothing.sensitive = false
2002             else
2003                 nothing.sensitive = true
2004             end
2005         end
2006     }
2007     $enhance.signal_connect('toggled') {
2008         if $enhance.active?
2009             set_mousecursor(Gdk::Cursor::SPRAYCAN)
2010             one_click_explain_try.call
2011             $r90.active = false
2012             $r270.active = false
2013             $delete.active = false
2014             nothing.sensitive = true
2015         else
2016             if !$r90.active? && !$r270.active? && !$delete.active?
2017                 set_mousecursor_normal
2018                 nothing.sensitive = false
2019             else
2020                 nothing.sensitive = true
2021             end
2022         end
2023     }
2024     $delete.signal_connect('toggled') {
2025         if $delete.active?
2026             set_mousecursor(Gdk::Cursor::PIRATE)
2027             one_click_explain_try.call
2028             $r90.active = false
2029             $r270.active = false
2030             $enhance.active = false
2031             nothing.sensitive = true
2032         else
2033             if !$r90.active? && !$r270.active? && !$enhance.active?
2034                 set_mousecursor_normal
2035                 nothing.sensitive = false
2036             else
2037                 nothing.sensitive = true
2038             end
2039         end
2040     }
2041     nothing.signal_connect('clicked') {
2042         $r90.active = $r270.active = $enhance.active = $delete.active = false
2043         set_mousecursor_normal
2044     }
2045
2046     return [ mb, tb ]
2047 end
2048
2049 def create_main_window
2050
2051     mb, tb = create_menu_and_toolbar
2052
2053 #    open_file('/home/gc/booh/foo')
2054
2055     $albums_tv = Gtk::TreeView.new
2056     $albums_tv.set_size_request(120, -1)
2057     renderer = Gtk::CellRendererText.new
2058     column = Gtk::TreeViewColumn.new('', renderer, { :text => 0 })
2059     $albums_tv.append_column(column)
2060     $albums_tv.set_headers_visible(false)
2061     $albums_tv.selection.signal_connect('changed') { |w|
2062         push_mousecursor_wait
2063         save_changes
2064         iter = w.selected
2065         if !iter
2066             msg 3, "no selection"
2067         else
2068             $current_path = $albums_ts.get_value(iter, 1)
2069             change_dir
2070         end
2071         pop_mousecursor
2072     }
2073     $albums_ts = Gtk::TreeStore.new(String, String)
2074     $albums_tv.set_model($albums_ts)
2075     $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
2076
2077     albums_sw = Gtk::ScrolledWindow.new(nil, nil)
2078     albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
2079     albums_sw.add_with_viewport($albums_tv)
2080
2081     $notebook = Gtk::Notebook.new
2082     create_subalbums_page
2083     $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
2084     create_auto_table
2085     $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
2086     $notebook.show_all
2087     $notebook.signal_connect('switch-page') { |w, page, num|
2088         if num == 0
2089             $delete.active = false
2090             $delete.sensitive = false
2091         else
2092             $delete.sensitive = true
2093         end
2094     }
2095
2096     paned = Gtk::HPaned.new
2097     paned.pack1(albums_sw, false, false)
2098     paned.pack2($notebook, true, true)
2099
2100     main_vbox = Gtk::VBox.new(false, 0)
2101     main_vbox.pack_start(mb, false, false)
2102     main_vbox.pack_start(tb, false, false)
2103     main_vbox.pack_start(paned, true, true)
2104     main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
2105
2106     $main_window = Gtk::Window.new
2107     $main_window.add(main_vbox)
2108     $main_window.signal_connect('delete-event') {
2109         try_quit
2110     }
2111
2112     #- read/save size and position of window
2113     if $config['pos-x'] && $config['pos-y']
2114         $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
2115     else
2116         $main_window.window_position = Gtk::Window::POS_CENTER
2117     end
2118     msg 3, "size: #{$config['width']}x#{$config['height']}"
2119     $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
2120     $main_window.signal_connect('configure-event') {
2121         msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
2122         x, y = $main_window.window.root_origin
2123         width, height = $main_window.window.size
2124         $config['pos-x'] = x
2125         $config['pos-y'] = y
2126         $config['width'] = width
2127         $config['height'] = height
2128         false
2129     }
2130
2131     $statusbar.push(0, utf8(_("Ready.")))
2132     $main_window.show_all
2133 end
2134
2135 Thread.abort_on_exception = true
2136 handle_options
2137 read_config
2138
2139 Gtk.init
2140 create_main_window
2141 if ARGV[0]
2142     open_file(ARGV[0])
2143 end
2144 Gtk.main
2145
2146 write_config