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