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