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