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