dont abort when reordering subalbums when there is a deleted directory there
[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                 if (!element.attributes['deleted'])
1786                     elems << [ element.attributes['path'], element.remove ]
1787                 end
1788             }
1789             elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
1790                   each { |e| $xmldir.add_element(e[1]) }
1791             #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
1792             $xmldir.elements.each('descendant::dir') { |elem|
1793                 elem.delete_attribute('already-generated')
1794             }
1795             change_dir
1796         }
1797
1798         color_swap_and_cleanup = Proc.new {
1799             perform_color_swap_and_cleanup = Proc.new {
1800                 color_swap(xmldir, "#{infotype}-")
1801                 my_gen_real_thumbnail.call
1802             }
1803             perform_color_swap_and_cleanup.call
1804
1805             save_undo(_("color swap"),
1806                       Proc.new {
1807                           perform_color_swap_and_cleanup.call
1808                           $notebook.set_page(0)
1809                           Proc.new {
1810                               perform_color_swap_and_cleanup.call
1811                               $notebook.set_page(0)
1812                           }
1813                       })
1814         }
1815
1816         change_frame_offset_and_cleanup = Proc.new {
1817             if values = ask_new_frame_offset(xmldir, "#{infotype}-")
1818                 perform_change_frame_offset_and_cleanup = Proc.new { |val|
1819                     change_frame_offset(xmldir, "#{infotype}-", val)
1820                     my_gen_real_thumbnail.call
1821                 }
1822                 perform_change_frame_offset_and_cleanup.call(values[:new])
1823
1824                 save_undo(_("specify frame offset"),
1825                           Proc.new {
1826                               perform_change_frame_offset_and_cleanup.call(values[:old])
1827                               $notebook.set_page(0)
1828                               Proc.new {
1829                                   perform_change_frame_offset_and_cleanup.call(values[:new])
1830                                   $notebook.set_page(0)
1831                               }
1832                           })
1833             end
1834         }
1835
1836         whitebalance_and_cleanup = Proc.new {
1837             if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
1838                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
1839                 perform_change_whitebalance_and_cleanup = Proc.new { |val|
1840                     change_whitebalance(xmldir, "#{infotype}-", val)
1841                     recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
1842                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
1843                     system("rm -f '#{thumbnail_file}'")
1844                 }
1845                 perform_change_whitebalance_and_cleanup.call(values[:new])
1846                 
1847                 save_undo(_("fix white balance"),
1848                           Proc.new {
1849                               perform_change_whitebalance_and_cleanup.call(values[:old])
1850                               $notebook.set_page(0)
1851                               Proc.new {
1852                                   perform_change_whitebalance_and_cleanup.call(values[:new])
1853                                   $notebook.set_page(0)
1854                               }
1855                           })
1856             end
1857         }
1858
1859         enhance_and_cleanup = Proc.new {
1860             perform_enhance_and_cleanup = Proc.new {
1861                 enhance(xmldir, "#{infotype}-")
1862                 my_gen_real_thumbnail.call
1863             }
1864             
1865             perform_enhance_and_cleanup.call
1866             
1867             save_undo(_("enhance"),
1868                       Proc.new {
1869                           perform_enhance_and_cleanup.call
1870                           $notebook.set_page(0)
1871                           Proc.new {
1872                               perform_enhance_and_cleanup.call
1873                               $notebook.set_page(0)
1874                           }
1875                       })
1876         }
1877
1878         evtbox.signal_connect('button-press-event') { |w, event|
1879             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1880                 if $r90.active?
1881                     rotate_and_cleanup.call(90)
1882                 elsif $r270.active?
1883                     rotate_and_cleanup.call(-90)
1884                 elsif $enhance.active?
1885                     enhance_and_cleanup.call
1886                 end
1887             end
1888             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1889                 popup_thumbnail_menu(event, ['change_image'], entry2type(captionfile), xmldir, "#{infotype}-",
1890                                      { :forbid_left => true, :forbid_right => true,
1891                                        :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter },
1892                                      { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
1893                                        :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
1894             end
1895             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1896                 change_image.call
1897                 true   #- handled
1898             end
1899         }
1900         evtbox.signal_connect('button-press-event') { |w, event|
1901             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
1902             false
1903         }
1904
1905         evtbox.signal_connect('button-release-event') { |w, event|
1906             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
1907                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
1908                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
1909                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
1910                     msg 3, "gesture rotate: #{angle}"
1911                     rotate_and_cleanup.call(angle)
1912                 end
1913             end
1914             $gesture_press = nil
1915         }
1916                 
1917         $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
1918         $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
1919         current_y_sub_albums += 1
1920     }
1921
1922     if $xmldir.child_byname_notattr('dir', 'deleted')
1923         #- title edition
1924         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
1925         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
1926         $subalbums_title.set_justification(Gtk::Justification::CENTER)
1927         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1928         #- this album image/caption
1929         if $xmldir.attributes['thumbnails-caption']
1930             add_subalbum.call($xmldir, 0)
1931         end
1932     end
1933     total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
1934     $xmldir.elements.each { |element|
1935         if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
1936             #- element (image or video) of this album
1937             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
1938             msg 3, "dest_img: #{dest_img}"
1939             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
1940             total[element.name] += 1
1941         end
1942         if element.name == 'dir' && !element.attributes['deleted']
1943             #- sub-album image/caption
1944             add_subalbum.call(element, subalbums_counter += 1)
1945             total[element.name] += 1
1946         end
1947     }
1948     $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
1949                                                                                 total['image'], total['video'], total['dir'] ]))
1950     $subalbums_vb.add($subalbums)
1951     $subalbums_vb.show_all
1952
1953     if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
1954         $notebook.get_tab_label($autotable_sw).sensitive = false
1955         $notebook.set_page(0)
1956         $thumbnails_title.buffer.text = ''
1957     else
1958         $notebook.get_tab_label($autotable_sw).sensitive = true
1959         $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
1960     end
1961
1962     if !$xmldir.child_byname_notattr('dir', 'deleted')
1963         $notebook.get_tab_label($subalbums_sw).sensitive = false
1964         $notebook.set_page(1)
1965     else
1966         $notebook.get_tab_label($subalbums_sw).sensitive = true
1967     end
1968 end
1969
1970 def pixbuf_or_nil(filename)
1971     begin
1972         return Gdk::Pixbuf.new(filename)
1973     rescue
1974         return nil
1975     end
1976 end
1977
1978 def theme_choose(current)
1979     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
1980                              $main_window,
1981                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1982                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1983                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1984
1985     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
1986     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
1987     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
1988     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
1989     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
1990     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
1991     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
1992     treeview.signal_connect('button-press-event') { |w, event|
1993         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1994             dialog.response(Gtk::Dialog::RESPONSE_OK)
1995         end
1996     }
1997
1998     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
1999
2000     `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2001         dir.chomp!
2002         iter = model.append
2003         iter[0] = File.basename(dir)
2004         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2005         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2006         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2007         if File.basename(dir) == current
2008             treeview.selection.select_iter(iter)
2009         end
2010     }
2011
2012     dialog.set_default_size(700, 400)
2013     dialog.vbox.show_all
2014     dialog.run { |response|
2015         iter = treeview.selection.selected
2016         dialog.destroy
2017         if response == Gtk::Dialog::RESPONSE_OK && iter
2018             return model.get_value(iter, 0)
2019         end
2020     }
2021     return nil
2022 end
2023
2024 def populate_subalbums_treeview
2025     $albums_ts.clear
2026     $autotable.clear
2027     $subalbums_vb.children.each { |chld|
2028         $subalbums_vb.remove(chld)
2029     }
2030
2031     source = $xmldoc.root.attributes['source']
2032     msg 3, "source: #{source}"
2033
2034     xmldir = $xmldoc.elements['//dir']
2035     if !xmldir || xmldir.attributes['path'] != source
2036         msg 1, _("Corrupted booh file...")
2037         return
2038     end
2039
2040     append_dir_elem = Proc.new { |parent_iter, xmldir|
2041         child_iter = $albums_ts.append(parent_iter)
2042         child_iter[0] = File.basename(xmldir.attributes['path'])
2043         child_iter[1] = xmldir.attributes['path']
2044         msg 3, "puttin location: #{xmldir.attributes['path']}"
2045         xmldir.elements.each('dir') { |elem|
2046             if !elem.attributes['deleted']
2047                 append_dir_elem.call(child_iter, elem)
2048             end
2049         }
2050     }
2051     append_dir_elem.call(nil, xmldir)
2052
2053     $albums_tv.expand_all
2054     $albums_tv.selection.select_iter($albums_ts.iter_first)
2055 end
2056
2057 def open_file(filename)
2058
2059     $filename = nil
2060     $modified = false
2061     $current_path = nil   #- invalidate
2062     $modified_pixbufs = {}
2063     $albums_ts.clear
2064     $autotable.clear
2065     $subalbums_vb.children.each { |chld|
2066         $subalbums_vb.remove(chld)
2067     }
2068
2069     if !File.exists?(filename)
2070         return utf8(_("File not found."))
2071     end
2072
2073     begin
2074         $xmldoc = REXML::Document.new File.new(filename)
2075     rescue Exception
2076         $xmldoc = nil
2077     end
2078
2079     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2080         if entry2type(filename).nil?
2081             return utf8(_("Not a booh file!"))
2082         else
2083             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."))
2084         end
2085     end
2086
2087     if !source = $xmldoc.root.attributes['source']
2088         return utf8(_("Corrupted booh file..."))
2089     end
2090
2091     if !dest = $xmldoc.root.attributes['destination']
2092         return utf8(_("Corrupted booh file..."))
2093     end
2094
2095     if !theme = $xmldoc.root.attributes['theme']
2096         return utf8(_("Corrupted booh file..."))
2097     end
2098
2099     if $xmldoc.root.attributes['version'] != $VERSION
2100         msg 2, _("File's version %s, booh version now #{$VERSION}, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2101         mark_document_as_dirty
2102         $xmldoc.root.add_attribute('version', $VERSION)
2103     end
2104
2105     limit_sizes = $xmldoc.root.attributes['limit-sizes']
2106     optimizefor32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2107     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2108
2109     $filename = filename
2110     select_theme(theme, limit_sizes, optimizefor32, nperrow)
2111     $default_size['thumbnails'] =~ /(.*)x(.*)/
2112     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2113     $albums_thumbnail_size =~ /(.*)x(.*)/
2114     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2115
2116     populate_subalbums_treeview
2117
2118     $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge.sensitive = $generate.sensitive = $properties.sensitive = $remove_all_captions.sensitive = true
2119     return nil
2120 end
2121
2122 def open_file_user(filename)
2123     result = open_file(filename)
2124     if !result
2125         $config['last-opens'] ||= []
2126         if $config['last-opens'][-1] != utf8(filename)
2127             $config['last-opens'] << utf8(filename)
2128         end
2129         $orig_filename = $filename
2130         tmp = Tempfile.new("boohtemp")
2131         tmp.close!
2132         #- for security
2133         ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2134         ios.close
2135         $tempfiles << $filename << "#{$filename}.backup"
2136     else
2137         $orig_filename = nil
2138     end
2139     return result
2140 end
2141
2142 def open_file_popup
2143     if !ask_save_modifications(utf8(_("Save this album?")),
2144                                utf8(_("Do you want to save the changes to this album?")),
2145                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2146         return
2147     end
2148     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2149                                     nil,
2150                                     Gtk::FileChooser::ACTION_OPEN,
2151                                     nil,
2152                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2153     fc.add_shortcut_folder(File.expand_path("~/.booh"))
2154     fc.set_current_folder(File.expand_path("~/.booh"))
2155     fc.transient_for = $main_window
2156     ok = false
2157     while !ok
2158         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2159             push_mousecursor_wait(fc)
2160             msg = open_file_user(fc.filename)
2161             pop_mousecursor(fc)
2162             if msg
2163                 show_popup(fc, msg)
2164                 ok = false
2165             else
2166                 ok = true
2167             end
2168         else
2169             ok = true
2170         end
2171     end
2172     fc.destroy
2173 end
2174
2175 def additional_booh_options
2176     options = ''
2177     if $config['mproc']
2178         options += "--mproc #{$config['mproc'].to_i} "
2179     end
2180     if $config['emptycomments']
2181         options += "--empty-comments "
2182     end
2183     return options
2184 end
2185
2186 def new_album
2187     if !ask_save_modifications(utf8(_("Save this album?")),
2188                                utf8(_("Do you want to save the changes to this album?")),
2189                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2190         return
2191     end
2192     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2193                              $main_window,
2194                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2195                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2196                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2197     
2198     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2199     tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2200                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2201     tbl.attach(src = Gtk::Entry.new,
2202                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2203     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2204                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2205     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2206                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2207     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2208                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2209     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2210                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2211     tbl.attach(dest = Gtk::Entry.new,
2212                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2213     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2214                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2215     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2216                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2217     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2218                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2219     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2220                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2221
2222     tooltips = Gtk::Tooltips.new
2223     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2224     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2225                          pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2226     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2227                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
2228     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2229     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)
2230     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2231                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2232
2233     src_nb_calculated_for = ''
2234     src_nb_thread = nil
2235     process_src_nb = Proc.new {
2236         if src.text != src_nb_calculated_for
2237             src_nb_calculated_for = src.text
2238             if src_nb_thread
2239                 Thread.kill(src_nb_thread)
2240                 src_nb_thread = nil
2241             end
2242             if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2243                 if File.readable?(from_utf8(src_nb_calculated_for))
2244                     src_nb_thread = Thread.new {
2245                         gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2246                         total = { 'image' => 0, 'video' => 0, nil => 0 }
2247                         `find '#{from_utf8(src_nb_calculated_for)}' -type d -follow`.each { |dir|
2248                             if File.basename(dir) =~ /^\./
2249                                 next
2250                             else
2251                                 begin
2252                                     Dir.entries(dir.chomp).each { |file|
2253                                         total[entry2type(file)] += 1
2254                                     }
2255                                 rescue Errno::EACCES, Errno::ENOENT
2256                                 end
2257                             end
2258                         }
2259                         gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2260                         src_nb_thread = nil
2261                     }
2262                 else
2263                     src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2264                 end
2265             else
2266                 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2267             end
2268         end
2269         true
2270     }
2271     timeout_src_nb = Gtk.timeout_add(100) {
2272         process_src_nb.call
2273     }
2274
2275     src_browse.signal_connect('clicked') {
2276         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2277                                         nil,
2278                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
2279                                         nil,
2280                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2281         fc.transient_for = $main_window
2282         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2283             src.text = utf8(fc.filename)
2284             process_src_nb.call
2285             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2286         end
2287         fc.destroy
2288     }
2289
2290     dest_browse.signal_connect('clicked') {
2291         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2292                                         nil,
2293                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
2294                                         nil,
2295                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2296         fc.transient_for = $main_window
2297         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2298             dest.text = utf8(fc.filename)
2299         end
2300         fc.destroy
2301     }
2302
2303     conf_browse.signal_connect('clicked') {
2304         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2305                                         nil,
2306                                         Gtk::FileChooser::ACTION_SAVE,
2307                                         nil,
2308                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2309         fc.transient_for = $main_window
2310         fc.add_shortcut_folder(File.expand_path("~/.booh"))
2311         fc.set_current_folder(File.expand_path("~/.booh"))
2312         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2313             conf.text = utf8(fc.filename)
2314         end
2315         fc.destroy
2316     }
2317
2318     theme_sizes = []
2319     nperrows = []
2320     recreate_theme_config = proc {
2321         theme_sizes.each { |e| sizes.remove(e[:widget]) }
2322         theme_sizes = []
2323         select_theme(theme_button.label, 'all', optimize432.active?, nil)
2324         $images_size.each { |s|
2325             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2326             if !s['optional']
2327                 cb.active = true
2328             end
2329             tooltips.set_tip(cb, utf8(s['description']), nil)
2330             theme_sizes << { :widget => cb, :value => s['name'] }
2331         }
2332         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2333         tooltips = Gtk::Tooltips.new
2334         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2335         theme_sizes << { :widget => cb, :value => 'original' }
2336         sizes.show_all
2337
2338         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2339         nperrow_group = nil
2340         nperrows = []
2341         $allowed_N_values.each { |n|
2342             if nperrow_group
2343                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2344             else
2345                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2346             end
2347             if $default_N == n
2348                 rb.active = true
2349             end
2350             nperrows << { :widget => rb, :value => n }
2351         }
2352         nperrowradios.show_all
2353     }
2354     recreate_theme_config.call
2355
2356     theme_button.signal_connect('clicked') {
2357         if newtheme = theme_choose(theme_button.label)
2358             theme_button.label = newtheme
2359             recreate_theme_config.call
2360         end
2361     }
2362
2363     dialog.vbox.add(frame1)
2364     dialog.vbox.add(frame2)
2365     dialog.window_position = Gtk::Window::POS_MOUSE
2366     dialog.show_all
2367
2368     keepon = true
2369     ok = true
2370     while keepon
2371         dialog.run { |response|
2372             if response == Gtk::Dialog::RESPONSE_OK
2373                 srcdir = from_utf8(src.text)
2374                 destdir = from_utf8(dest.text)
2375                 if !File.directory?(srcdir)
2376                     show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2377                     src.grab_focus
2378                 elsif conf.text == ''
2379                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2380                     conf.grab_focus
2381                 elsif destdir != make_dest_filename(destdir)
2382                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2383                     dest.grab_focus
2384                 elsif File.directory?(destdir)
2385                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2386                     dest.grab_focus
2387                 elsif File.exists?(destdir)
2388                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2389                     dest.grab_focus
2390                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2391                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2392                 else
2393                     system("mkdir '#{destdir}'")
2394                     if !File.directory?(destdir)
2395                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2396                         dest.grab_focus
2397                     else
2398                         keepon = false
2399                     end
2400                 end
2401             else
2402                 keepon = ok = false
2403             end
2404         }
2405     end
2406     srcdir = from_utf8(src.text)
2407     destdir = from_utf8(dest.text)
2408     configskel = File.expand_path(from_utf8(conf.text))
2409     theme = theme_button.label
2410     sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2411     nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2412     opt432 = optimize432.active?
2413     if src_nb_thread
2414         Thread.kill(src_nb_thread)
2415         gtk_thread_abandon  #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2416     end
2417     dialog.destroy
2418     Gtk.timeout_remove(timeout_src_nb)
2419
2420     if ok
2421         call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2422                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2423                      "#{opt432 ? '--optimize-for-32' : ''} #{additional_booh_options}",
2424                      utf8(_("Please wait while scanning source directory...")),
2425                      'full scan',
2426                      { :closure_after => proc { open_file_user(configskel) } })
2427     end
2428 end
2429
2430 def properties
2431     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2432                              $main_window,
2433                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2434                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2435                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2436     
2437     source = $xmldoc.root.attributes['source']
2438     dest = $xmldoc.root.attributes['destination']
2439     theme = $xmldoc.root.attributes['theme']
2440     opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2441     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2442     limit_sizes = $xmldoc.root.attributes['limit-sizes']
2443     if limit_sizes
2444         limit_sizes = limit_sizes.split(/,/)
2445     end
2446
2447     tooltips = Gtk::Tooltips.new
2448     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2449     tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2450                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2451     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2452                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2453     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2454                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2455     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>')),
2456                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2457     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2458                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2459     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>')),
2460                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2461
2462     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2463     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2464                          pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2465     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2466                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
2467     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2468     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)
2469     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2470                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2471
2472     theme_sizes = []
2473     nperrows = []
2474     recreate_theme_config = proc {
2475         theme_sizes.each { |e| sizes.remove(e[:widget]) }
2476         theme_sizes = []
2477         select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
2478
2479         $images_size.each { |s|
2480             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2481             if limit_sizes
2482                 if limit_sizes.include?(s['name'])
2483                     cb.active = true
2484                 end
2485             else
2486                 if !s['optional']
2487                     cb.active = true
2488                 end
2489             end
2490             tooltips.set_tip(cb, utf8(s['description']), nil)
2491             theme_sizes << { :widget => cb, :value => s['name'] }
2492         }
2493         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2494         tooltips = Gtk::Tooltips.new
2495         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2496         if limit_sizes && limit_sizes.include?('original')
2497             cb.active = true
2498         end
2499         theme_sizes << { :widget => cb, :value => 'original' }
2500         sizes.show_all
2501
2502         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2503         nperrow_group = nil
2504         nperrows = []
2505         $allowed_N_values.each { |n|
2506             if nperrow_group
2507                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2508             else
2509                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2510             end
2511             nperrowradios.add(Gtk::Label.new('  '))
2512             if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
2513                 rb.active = true
2514             end
2515             nperrows << { :widget => rb, :value => n.to_s }
2516         }
2517         nperrowradios.show_all
2518     }
2519     recreate_theme_config.call
2520
2521     theme_button.signal_connect('clicked') {
2522         if newtheme = theme_choose(theme_button.label)
2523             limit_sizes = nil
2524             nperrow = nil
2525             theme_button.label = newtheme
2526             recreate_theme_config.call
2527         end
2528     }
2529
2530     dialog.vbox.add(frame1)
2531     dialog.vbox.add(frame2)
2532     dialog.window_position = Gtk::Window::POS_MOUSE
2533     dialog.show_all
2534
2535     keepon = true
2536     ok = true
2537     while keepon
2538         dialog.run { |response|
2539             if response == Gtk::Dialog::RESPONSE_OK
2540                 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2541                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2542                 else
2543                     keepon = false
2544                 end
2545             else
2546                 keepon = ok = false
2547             end
2548         }
2549     end
2550     save_theme = theme_button.label
2551     save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
2552     save_opt432 = optimize432.active?
2553     save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2554     dialog.destroy
2555
2556     if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow)
2557         mark_document_as_dirty
2558         save_current_file
2559         call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
2560                      "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
2561                      "#{save_opt432 ? '--optimize-for-32' : ''} #{additional_booh_options}",
2562                      utf8(_("Please wait while scanning source directory...")),
2563                      'full scan',
2564                      { :closure_after => proc {
2565                              open_file($filename)
2566                              $modified = true
2567                          } })
2568     end
2569 end
2570
2571 def merge_current
2572     save_current_file
2573
2574     sel = $albums_tv.selection.selected_rows
2575
2576     call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2577                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
2578                  utf8(_("Please wait while scanning source directory...")),
2579                  'one dir scan',
2580                  { :closure_after => proc {
2581                          open_file($filename)
2582                          $albums_tv.selection.select_path(sel[0])
2583                          $modified = true
2584                      } })
2585 end
2586
2587 def merge
2588     save_current_file
2589
2590     theme = $xmldoc.root.attributes['theme']
2591     limit_sizes = $xmldoc.root.attributes['limit-sizes']
2592     if limit_sizes
2593         limit_sizes = "--sizes #{limit_sizes}"
2594     end
2595     call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
2596                  "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
2597                  utf8(_("Please wait while scanning source directory...")),
2598                  'full scan',
2599                  { :closure_after => proc {
2600                          open_file($filename)
2601                          $modified = true
2602                      } })
2603 end
2604
2605 def save_as_do
2606     fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
2607                                     nil,
2608                                     Gtk::FileChooser::ACTION_SAVE,
2609                                     nil,
2610                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2611     fc.transient_for = $main_window
2612     fc.add_shortcut_folder(File.expand_path("~/.booh"))
2613     fc.set_current_folder(File.expand_path("~/.booh"))
2614     fc.filename = $orig_filename
2615     if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2616         $orig_filename = fc.filename
2617         save_current_file_user
2618     end
2619     fc.destroy
2620 end
2621
2622 def preferences
2623     dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
2624                              $main_window,
2625                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2626                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2627                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2628
2629     dialog.vbox.add(notebook = Gtk::Notebook.new)
2630     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
2631     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
2632                0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2633     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
2634                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2635     tooltips = Gtk::Tooltips.new
2636     tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename; for example: mplayer %f")), nil)
2637     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
2638                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2639     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)),
2640                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2641     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)
2642     tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
2643                0, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2644     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)
2645     tbl.attach(emptycomments_check = Gtk::CheckButton.new(utf8(_("Use empty comments for new albums"))),
2646                0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2647     tooltips.set_tip(emptycomments_check, utf8(_("Normally, filenames are used as comments for new albums. Check this if you prefer empty comments.")), nil)
2648     smp_check.signal_connect('toggled') {
2649         if smp_check.active?
2650             smp_hbox.sensitive = true
2651         else
2652             smp_hbox.sensitive = false
2653         end
2654     }
2655     if $config['mproc']
2656         smp_check.active = true
2657         smp_spin.value = $config['mproc'].to_i
2658     end
2659     nogestures_check.active = $config['nogestures']
2660     emptycomments_check.active = $config['emptycomments']
2661
2662     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
2663     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
2664                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2665     tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
2666                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2667
2668     dialog.vbox.show_all
2669     dialog.run { |response|
2670         if response == Gtk::Dialog::RESPONSE_OK
2671             $config['video-viewer'] = video_viewer_entry.text
2672             if smp_check.active?
2673                 $config['mproc'] = smp_spin.value.to_i
2674             else
2675                 $config.delete('mproc')
2676             end
2677             $config['nogestures'] = nogestures_check.active?
2678             $config['emptycomments'] = emptycomments_check.active?
2679
2680             $config['convert-enhance'] = enhance_entry.text
2681         end
2682     }
2683     dialog.destroy
2684 end
2685
2686 def perform_undo
2687     if $undo_tb.sensitive?
2688         $redo_tb.sensitive = $redo_mb.sensitive = true
2689         if not more_undoes = UndoHandler.undo($statusbar)
2690             $undo_tb.sensitive = $undo_mb.sensitive = false
2691         end
2692     end
2693 end
2694
2695 def perform_redo
2696     if $redo_tb.sensitive?
2697         $undo_tb.sensitive = $undo_mb.sensitive = true
2698         if not more_redoes = UndoHandler.redo($statusbar)
2699             $redo_tb.sensitive = $redo_mb.sensitive = false
2700         end
2701     end
2702 end
2703
2704 def show_one_click_explanation(intro)
2705     show_popup($main_window, utf8(_("<b>One-Click tools.</b>
2706
2707 %s When such a tool is activated
2708 (<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
2709 on a thumbnail will immediately apply the desired action.
2710
2711 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
2712 ") % intro))
2713 end
2714
2715 def create_menu_and_toolbar
2716
2717     #- menu
2718     mb = Gtk::MenuBar.new
2719
2720     filemenu = Gtk::MenuItem.new(utf8(_("_File")))
2721     filesubmenu = Gtk::Menu.new
2722     filesubmenu.append(new       = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
2723     filesubmenu.append(open      = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
2724     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2725     filesubmenu.append($save     = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
2726     filesubmenu.append($save_as  = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
2727     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2728     tooltips = Gtk::Tooltips.new
2729     filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
2730     $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
2731     tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
2732     filesubmenu.append($merge    = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
2733     $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
2734     tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums")), nil)
2735     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2736     filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
2737     $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
2738     tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
2739     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2740     filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
2741     tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
2742     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2743     filesubmenu.append(quit      = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
2744     filemenu.set_submenu(filesubmenu)
2745     mb.append(filemenu)
2746
2747     new.signal_connect('activate') { new_album }
2748     open.signal_connect('activate') { open_file_popup }
2749     $save.signal_connect('activate') { save_current_file_user }
2750     $save_as.signal_connect('activate') { save_as_do }
2751     $merge_current.signal_connect('activate') { merge_current }
2752     $merge.signal_connect('activate') { merge }
2753     $generate.signal_connect('activate') {
2754         save_current_file
2755         call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
2756                      utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
2757                      'web-album',
2758                      { :successmsg => utf8(_("Your web-album is now ready in directory `%s'.") % $xmldoc.root.attributes['destination']),
2759                        :failuremsg => utf8(_("There was something wrong when generating the web-album, sorry.")),
2760                        :closure_after => proc {
2761                              $xmldoc.elements.each('//dir') { |elem|
2762                                  elem.add_attribute('already-generated', 'true')
2763                              }
2764                              UndoHandler.cleanup   #- prevent save_changes to mark current dir as not already generated
2765                              $undo_tb.sensitive = $undo_mb.sensitive = false
2766                              $redo_tb.sensitive = $redo_mb.sensitive = false
2767                              save_current_file
2768                              $generated_outofline = true
2769                          }})
2770     }
2771     $properties.signal_connect('activate') { properties }
2772
2773     quit.signal_connect('activate') { try_quit }
2774
2775     editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
2776     editsubmenu = Gtk::Menu.new
2777     editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
2778     editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
2779     editsubmenu.append(           Gtk::SeparatorMenuItem.new)
2780     editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
2781     $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
2782     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)
2783     editsubmenu.append(           Gtk::SeparatorMenuItem.new)
2784     editsubmenu.append(prefs    = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
2785     editmenu.set_submenu(editsubmenu)
2786     mb.append(editmenu)
2787
2788     $remove_all_captions.signal_connect('activate') { remove_all_captions }
2789
2790     prefs.signal_connect('activate') { preferences }
2791
2792     helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
2793     helpsubmenu = Gtk::Menu.new
2794     helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
2795     one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
2796     helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
2797     speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
2798     helpsubmenu.append(        Gtk::SeparatorMenuItem.new)
2799     helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
2800     helpmenu.set_submenu(helpsubmenu)
2801     mb.append(helpmenu)
2802
2803     one_click.signal_connect('activate') {
2804         show_one_click_explanation(_("One-Click tools are available in the toolbar."))
2805     }
2806
2807     speed.signal_connect('activate') {
2808         show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
2809
2810 <span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
2811 <span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
2812 <span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
2813 <span foreground='darkblue'>Control-Enter</span>: for an image, open larger view; for a video, launch player
2814 <span foreground='darkblue'>Control-Delete</span>: delete image
2815 <span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
2816 <span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
2817 <span foreground='darkblue'>Control-z</span>: undo
2818 <span foreground='darkblue'>Control-r</span>: redo
2819
2820 <span size='large' weight='bold'>Mouse gestures:</span>
2821
2822 Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
2823 for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
2824
2825 <span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
2826 <span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
2827 <span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
2828 <span foreground='darkblue'>Left click, hold left button, right click</span>: undo
2829 <span foreground='darkblue'>Right click, hold right button, left click</span>: redo
2830 ")), { :pos_centered => true, :not_transient => true })
2831     }
2832
2833
2834     about.signal_connect('activate') {
2835         show_popup($main_window, utf8(_("<span size='x-large' weight='bold'>Booh %s</span>
2836
2837 <i>``The Web-Album of choice for discriminating Linux users''</i>
2838
2839 Copyright (c) 2005 Guillaume Cottenceau
2840
2841 Artwork: Ayo73
2842
2843 Translations:
2844 Japanese: Masao Mutoh
2845 French: Guillaume Cottenceau") % $VERSION), { :centered => true, :pos_centered => true, :topwidget => Gtk::Image.new("#{$FPATH}/images/logo.png") })
2846     }
2847
2848
2849     #- toolbar
2850     tb = Gtk::Toolbar.new
2851
2852     tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
2853     open.label = utf8(_("Open"))  #- to avoid missing gtk2 l10n catalogs
2854     open.menu = Gtk::Menu.new
2855     open.signal_connect('clicked') { open_file_popup }
2856     open.signal_connect('show-menu') {
2857         lastopens = Gtk::Menu.new
2858         j = 0
2859         if $config['last-opens']
2860             $config['last-opens'].reverse.each { |e|
2861                 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
2862                 item.signal_connect('activate') {
2863                     if ask_save_modifications(utf8(_("Save this album?")),
2864                                               utf8(_("Do you want to save the changes to this album?")),
2865                                               { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2866                         push_mousecursor_wait
2867                         msg = open_file_user(from_utf8(e))
2868                         pop_mousecursor
2869                         if msg
2870                             show_popup($main_window, msg)
2871                         end
2872                     end
2873                 }
2874                 j += 1
2875             }
2876             lastopens.show_all
2877         end
2878         open.menu = lastopens
2879     }
2880
2881     tb.insert(-1, Gtk::SeparatorToolItem.new)
2882
2883     tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
2884     $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
2885     $r90.label = utf8(_("Rotate"))
2886     tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
2887     $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
2888     $r270.label = utf8(_("Rotate"))
2889     tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
2890     $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
2891     $enhance.label = utf8(_("Enhance"))
2892     tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
2893     $delete.label = utf8(_("Delete"))  #- to avoid missing gtk2 l10n catalogs
2894     tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
2895     nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
2896     nothing.label = utf8(_("None"))
2897
2898     tb.insert(-1, Gtk::SeparatorToolItem.new)
2899
2900     tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
2901     tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
2902
2903
2904     $undo_tb.signal_connect('clicked')  { perform_undo }
2905     $undo_mb.signal_connect('activate') { perform_undo }
2906     $redo_tb.signal_connect('clicked')  { perform_redo }
2907     $redo_mb.signal_connect('activate') { perform_redo }
2908
2909     one_click_explain_try = Proc.new {
2910         if !$config['one-click-explained']
2911             show_one_click_explanation(_("You have just clicked on a One-Click tool."))
2912             $config['one-click-explained'] = true
2913         end
2914     }
2915
2916     $r90.signal_connect('toggled') {
2917         if $r90.active?
2918             set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
2919             one_click_explain_try.call
2920             $r270.active = false
2921             $enhance.active = false
2922             $delete.active = false
2923             nothing.sensitive = true
2924         else
2925             if !$r270.active? && !$enhance.active? && !$delete.active?
2926                 set_mousecursor_normal
2927                 nothing.sensitive = false
2928             else
2929                 nothing.sensitive = true
2930             end
2931         end
2932     }
2933     $r270.signal_connect('toggled') {
2934         if $r270.active?
2935             set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
2936             one_click_explain_try.call
2937             $r90.active = false
2938             $enhance.active = false
2939             $delete.active = false
2940             nothing.sensitive = true
2941         else
2942             if !$r90.active? && !$enhance.active? && !$delete.active?
2943                 set_mousecursor_normal
2944                 nothing.sensitive = false
2945             else
2946                 nothing.sensitive = true
2947             end
2948         end
2949     }
2950     $enhance.signal_connect('toggled') {
2951         if $enhance.active?
2952             set_mousecursor(Gdk::Cursor::SPRAYCAN)
2953             one_click_explain_try.call
2954             $r90.active = false
2955             $r270.active = false
2956             $delete.active = false
2957             nothing.sensitive = true
2958         else
2959             if !$r90.active? && !$r270.active? && !$delete.active?
2960                 set_mousecursor_normal
2961                 nothing.sensitive = false
2962             else
2963                 nothing.sensitive = true
2964             end
2965         end
2966     }
2967     $delete.signal_connect('toggled') {
2968         if $delete.active?
2969             set_mousecursor(Gdk::Cursor::PIRATE)
2970             one_click_explain_try.call
2971             $r90.active = false
2972             $r270.active = false
2973             $enhance.active = false
2974             nothing.sensitive = true
2975         else
2976             if !$r90.active? && !$r270.active? && !$enhance.active?
2977                 set_mousecursor_normal
2978                 nothing.sensitive = false
2979             else
2980                 nothing.sensitive = true
2981             end
2982         end
2983     }
2984     nothing.signal_connect('clicked') {
2985         $r90.active = $r270.active = $enhance.active = $delete.active = false
2986         set_mousecursor_normal
2987     }
2988
2989     return [ mb, tb ]
2990 end
2991
2992 def gtk_thread_protect(&proc)
2993     if Thread.current == Thread.main
2994         proc.call
2995     else
2996         $protect_gtk_pending_calls.synchronize {
2997             $gtk_pending_calls << proc
2998         }
2999     end
3000 end
3001
3002 def gtk_thread_abandon
3003     $protect_gtk_pending_calls.try_lock
3004     $gtk_pending_calls = []
3005     $protect_gtk_pending_calls.unlock
3006 end
3007
3008 def create_main_window
3009
3010     mb, tb = create_menu_and_toolbar
3011
3012     $albums_tv = Gtk::TreeView.new
3013     $albums_tv.set_size_request(120, -1)
3014     renderer = Gtk::CellRendererText.new
3015     column = Gtk::TreeViewColumn.new('', renderer, { :text => 0 })
3016     $albums_tv.append_column(column)
3017     $albums_tv.set_headers_visible(false)
3018     $albums_tv.selection.signal_connect('changed') { |w|
3019         push_mousecursor_wait
3020         save_changes
3021         iter = w.selected
3022         if !iter
3023             msg 3, "no selection"
3024         else
3025             $current_path = $albums_ts.get_value(iter, 1)
3026             change_dir
3027         end
3028         pop_mousecursor
3029     }
3030     $albums_ts = Gtk::TreeStore.new(String, String)
3031     $albums_tv.set_model($albums_ts)
3032     $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
3033
3034     albums_sw = Gtk::ScrolledWindow.new(nil, nil)
3035     albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
3036     albums_sw.add_with_viewport($albums_tv)
3037
3038     $notebook = Gtk::Notebook.new
3039     create_subalbums_page
3040     $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
3041     create_auto_table
3042     $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
3043     $notebook.show_all
3044     $notebook.signal_connect('switch-page') { |w, page, num|
3045         if num == 0
3046             $delete.active = false
3047             $delete.sensitive = false
3048         else
3049             $delete.sensitive = true
3050         end
3051         if $xmldir && $subalbums_edits[$xmldir.attributes['path']] && textview = $subalbums_edits[$xmldir.attributes['path']][:editzone]
3052             if num == 0
3053                 textview.buffer.text = $thumbnails_title.buffer.text
3054             else
3055                 if $notebook.get_tab_label($autotable_sw).sensitive?
3056                     $thumbnails_title.buffer.text = textview.buffer.text
3057                 end
3058             end
3059         end
3060     }
3061
3062     paned = Gtk::HPaned.new
3063     paned.pack1(albums_sw, false, false)
3064     paned.pack2($notebook, true, true)
3065
3066     main_vbox = Gtk::VBox.new(false, 0)
3067     main_vbox.pack_start(mb, false, false)
3068     main_vbox.pack_start(tb, false, false)
3069     main_vbox.pack_start(paned, true, true)
3070     main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
3071
3072     $main_window = Gtk::Window.new
3073     $main_window.add(main_vbox)
3074     $main_window.signal_connect('delete-event') {
3075         try_quit({ :disallow_cancel => true })
3076     }
3077
3078     #- read/save size and position of window
3079     if $config['pos-x'] && $config['pos-y']
3080         $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
3081     else
3082         $main_window.window_position = Gtk::Window::POS_CENTER
3083     end
3084     msg 3, "size: #{$config['width']}x#{$config['height']}"
3085     $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
3086     $main_window.signal_connect('configure-event') {
3087         msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
3088         x, y = $main_window.window.root_origin
3089         width, height = $main_window.window.size
3090         $config['pos-x'] = x
3091         $config['pos-y'] = y
3092         $config['width'] = width
3093         $config['height'] = height
3094         false
3095     }
3096
3097     $protect_gtk_pending_calls = Mutex.new
3098     $gtk_pending_calls = []
3099     Gtk.timeout_add(100) {
3100         $protect_gtk_pending_calls.synchronize {
3101             $gtk_pending_calls.each { |c| c.call }
3102             $gtk_pending_calls = []
3103         }
3104         true
3105     }
3106
3107     $statusbar.push(0, utf8(_("Ready.")))
3108     $main_window.show_all
3109 end
3110
3111 Thread.abort_on_exception = true
3112
3113 handle_options
3114 read_config
3115
3116 Gtk.init
3117 create_main_window
3118 if ARGV[0]
3119     open_file_user(ARGV[0])
3120 end
3121 Gtk.main
3122
3123 write_config