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