small fixes related to file saves, filenames when invoking new album
[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     if !ask_save_modifications(utf8(_("Save this album?")),
2172                                utf8(_("Do you want to save the changes to this album?")),
2173                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2174         return
2175     end
2176     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
2177                              $main_window,
2178                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2179                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2180                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2181     
2182     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2183     tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
2184                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2185     tbl.attach(src = Gtk::Entry.new,
2186                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2187     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
2188                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2189     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
2190                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2191     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
2192                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2193     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
2194                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2195     tbl.attach(dest = Gtk::Entry.new,
2196                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2197     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
2198                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2199     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
2200                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2201     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
2202                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2203     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
2204                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2205
2206     tooltips = Gtk::Tooltips.new
2207     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2208     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2209                          pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
2210     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2211                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
2212     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
2213     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)
2214     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2215                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2216
2217     src_nb_calculated_for = ''
2218     src_nb_thread = nil
2219     process_src_nb = Proc.new {
2220         if src.text != src_nb_calculated_for
2221             src_nb_calculated_for = src.text
2222             if src_nb_thread
2223                 Thread.kill(src_nb_thread)
2224                 src_nb_thread = nil
2225             end
2226             if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
2227                 if File.readable?(from_utf8(src_nb_calculated_for))
2228                     src_nb_thread = Thread.new {
2229                         gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
2230                         total = { 'image' => 0, 'video' => 0, nil => 0 }
2231                         `find '#{from_utf8(src_nb_calculated_for)}' -type d`.each { |dir|
2232                             if File.basename(dir) =~ /^\./
2233                                 next
2234                             else
2235                                 begin
2236                                     Dir.entries(dir.chomp).each { |file|
2237                                         total[entry2type(file)] += 1
2238                                     }
2239                                 rescue Errno::EACCES, Errno::ENOENT
2240                                 end
2241                             end
2242                         }
2243                         gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
2244                         src_nb_thread = nil
2245                     }
2246                 else
2247                     src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
2248                 end
2249             else
2250                 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
2251             end
2252         end
2253         true
2254     }
2255     timeout_src_nb = Gtk.timeout_add(100) {
2256         process_src_nb.call
2257     }
2258
2259     src_browse.signal_connect('clicked') {
2260         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
2261                                         nil,
2262                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
2263                                         nil,
2264                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2265         fc.transient_for = $main_window
2266         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2267             src.text = utf8(fc.filename)
2268             process_src_nb.call
2269             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
2270         end
2271         fc.destroy
2272     }
2273
2274     dest_browse.signal_connect('clicked') {
2275         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
2276                                         nil,
2277                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
2278                                         nil,
2279                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2280         fc.transient_for = $main_window
2281         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2282             dest.text = utf8(fc.filename)
2283         end
2284         fc.destroy
2285     }
2286
2287     conf_browse.signal_connect('clicked') {
2288         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
2289                                         nil,
2290                                         Gtk::FileChooser::ACTION_SAVE,
2291                                         nil,
2292                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2293         fc.transient_for = $main_window
2294         fc.add_shortcut_folder(File.expand_path("~/.booh"))
2295         fc.set_current_folder(File.expand_path("~/.booh"))
2296         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2297             conf.text = utf8(fc.filename)
2298         end
2299         fc.destroy
2300     }
2301
2302     theme_sizes = []
2303     nperrows = []
2304     recreate_theme_config = proc {
2305         theme_sizes.each { |e| sizes.remove(e[:widget]) }
2306         theme_sizes = []
2307         select_theme(theme_button.label, 'all', optimize432.active?, nil)
2308         $images_size.each { |s|
2309             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2310             if !s['optional']
2311                 cb.active = true
2312             end
2313             tooltips.set_tip(cb, utf8(s['description']), nil)
2314             theme_sizes << { :widget => cb, :value => s['name'] }
2315         }
2316         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2317         tooltips = Gtk::Tooltips.new
2318         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2319         theme_sizes << { :widget => cb, :value => 'original' }
2320         sizes.show_all
2321
2322         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2323         nperrow_group = nil
2324         nperrows = []
2325         $allowed_N_values.each { |n|
2326             if nperrow_group
2327                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2328             else
2329                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2330             end
2331             if $default_N == n
2332                 rb.active = true
2333             end
2334             nperrows << { :widget => rb, :value => n }
2335         }
2336         nperrowradios.show_all
2337     }
2338     recreate_theme_config.call
2339
2340     theme_button.signal_connect('clicked') {
2341         if newtheme = theme_choose(theme_button.label)
2342             theme_button.label = newtheme
2343             recreate_theme_config.call
2344         end
2345     }
2346
2347     dialog.vbox.add(frame1)
2348     dialog.vbox.add(frame2)
2349     dialog.window_position = Gtk::Window::POS_MOUSE
2350     dialog.show_all
2351
2352     keepon = true
2353     ok = true
2354     while keepon
2355         dialog.run { |response|
2356             if response == Gtk::Dialog::RESPONSE_OK
2357                 srcdir = from_utf8(src.text)
2358                 destdir = from_utf8(dest.text)
2359                 if !File.directory?(srcdir)
2360                     show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
2361                     src.grab_focus
2362                 elsif conf.text == ''
2363                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
2364                     conf.grab_focus
2365                 elsif destdir != make_dest_filename(destdir)
2366                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
2367                     dest.grab_focus
2368                 elsif File.directory?(destdir)
2369                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
2370                     dest.grab_focus
2371                 elsif File.exists?(destdir)
2372                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
2373                     dest.grab_focus
2374                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2375                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2376                 else
2377                     system("mkdir '#{destdir}'")
2378                     if !File.directory?(destdir)
2379                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
2380                         dest.grab_focus
2381                     else
2382                         keepon = false
2383                     end
2384                 end
2385             else
2386                 keepon = ok = false
2387             end
2388         }
2389     end
2390     srcdir = from_utf8(src.text)
2391     destdir = from_utf8(dest.text)
2392     configskel = File.expand_path(from_utf8(conf.text))
2393     theme = theme_button.label
2394     sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
2395     nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2396     opt432 = optimize432.active?
2397     if src_nb_thread
2398         Thread.kill(src_nb_thread)
2399         gtk_thread_abandon  #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
2400     end
2401     dialog.destroy
2402     Gtk.timeout_remove(timeout_src_nb)
2403
2404     if ok
2405         call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
2406                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
2407                      "#{opt432 ? '--optimize-for-32' : ''} #{additional_booh_options}",
2408                      utf8(_("Please wait while scanning source directory...")),
2409                      'full scan',
2410                      { :closure_after => proc { open_file_user(configskel) } })
2411     end
2412 end
2413
2414 def properties
2415     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
2416                              $main_window,
2417                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2418                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2419                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2420     
2421     source = $xmldoc.root.attributes['source']
2422     dest = $xmldoc.root.attributes['destination']
2423     theme = $xmldoc.root.attributes['theme']
2424     opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
2425     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
2426     limit_sizes = $xmldoc.root.attributes['limit-sizes']
2427     if limit_sizes
2428         limit_sizes = limit_sizes.split(/,/)
2429     end
2430
2431     tooltips = Gtk::Tooltips.new
2432     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
2433     tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
2434                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2435     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
2436                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2437     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
2438                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2439     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>')),
2440                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2441     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
2442                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2443     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>')),
2444                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2445
2446     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
2447     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
2448                          pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
2449     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
2450                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
2451     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
2452     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)
2453     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
2454                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
2455
2456     theme_sizes = []
2457     nperrows = []
2458     recreate_theme_config = proc {
2459         theme_sizes.each { |e| sizes.remove(e[:widget]) }
2460         theme_sizes = []
2461         select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
2462
2463         $images_size.each { |s|
2464             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
2465             if limit_sizes
2466                 if limit_sizes.include?(s['name'])
2467                     cb.active = true
2468                 end
2469             else
2470                 if !s['optional']
2471                     cb.active = true
2472                 end
2473             end
2474             tooltips.set_tip(cb, utf8(s['description']), nil)
2475             theme_sizes << { :widget => cb, :value => s['name'] }
2476         }
2477         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
2478         tooltips = Gtk::Tooltips.new
2479         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
2480         if limit_sizes && limit_sizes.include?('original')
2481             cb.active = true
2482         end
2483         theme_sizes << { :widget => cb, :value => 'original' }
2484         sizes.show_all
2485
2486         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
2487         nperrow_group = nil
2488         nperrows = []
2489         $allowed_N_values.each { |n|
2490             if nperrow_group
2491                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
2492             else
2493                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
2494             end
2495             nperrowradios.add(Gtk::Label.new('  '))
2496             if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
2497                 rb.active = true
2498             end
2499             nperrows << { :widget => rb, :value => n.to_s }
2500         }
2501         nperrowradios.show_all
2502     }
2503     recreate_theme_config.call
2504
2505     theme_button.signal_connect('clicked') {
2506         if newtheme = theme_choose(theme_button.label)
2507             limit_sizes = nil
2508             nperrow = nil
2509             theme_button.label = newtheme
2510             recreate_theme_config.call
2511         end
2512     }
2513
2514     dialog.vbox.add(frame1)
2515     dialog.vbox.add(frame2)
2516     dialog.window_position = Gtk::Window::POS_MOUSE
2517     dialog.show_all
2518
2519     keepon = true
2520     ok = true
2521     while keepon
2522         dialog.run { |response|
2523             if response == Gtk::Dialog::RESPONSE_OK
2524                 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
2525                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
2526                 else
2527                     keepon = false
2528                 end
2529             else
2530                 keepon = ok = false
2531             end
2532         }
2533     end
2534     save_theme = theme_button.label
2535     save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
2536     save_opt432 = optimize432.active?
2537     save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
2538     dialog.destroy
2539
2540     if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow)
2541         $xmldoc.elements.each('//dir') { |elem|
2542             elem.delete_attribute('already-generated')
2543         }
2544         save_current_file
2545         call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
2546                      "--thumbnails-per-row #{save_nperrow} --theme #{theme} --sizes #{save_limit_sizes.join(',')} " +
2547                      "#{save_opt432 ? '--optimize-for-32' : ''} #{additional_booh_options}",
2548                      utf8(_("Please wait while scanning source directory...")),
2549                      'full scan',
2550                      { :closure_after => proc {
2551                              open_file($filename)
2552                              $modified = true
2553                          } })
2554     end
2555 end
2556
2557 def merge_current
2558     save_current_file
2559
2560     sel = $albums_tv.selection.selected_rows
2561
2562     call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
2563                  "--verbose-level #{$verbose_level} #{additional_booh_options}",
2564                  utf8(_("Please wait while scanning source directory...")),
2565                  'one dir scan',
2566                  { :closure_after => proc {
2567                          open_file($filename)
2568                          $albums_tv.selection.select_path(sel[0])
2569                          $modified = true
2570                      } })
2571 end
2572
2573 def merge
2574     save_current_file
2575
2576     theme = $xmldoc.root.attributes['theme']
2577     limit_sizes = $xmldoc.root.attributes['limit-sizes']
2578     if limit_sizes
2579         limit_sizes = "--sizes #{limit_sizes}"
2580     end
2581     call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
2582                  "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
2583                  utf8(_("Please wait while scanning source directory...")),
2584                  'full scan',
2585                  { :closure_after => proc {
2586                          open_file($filename)
2587                          $modified = true
2588                      } })
2589 end
2590
2591 def save_as_do
2592     fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
2593                                     nil,
2594                                     Gtk::FileChooser::ACTION_SAVE,
2595                                     nil,
2596                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2597     fc.transient_for = $main_window
2598     fc.add_shortcut_folder(File.expand_path("~/.booh"))
2599     fc.set_current_folder(File.expand_path("~/.booh"))
2600     fc.filename = $orig_filename
2601     if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2602         $orig_filename = fc.filename
2603         save_current_file_user
2604     end
2605     fc.destroy
2606 end
2607
2608 def preferences
2609     dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
2610                              $main_window,
2611                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2612                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2613                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2614
2615     dialog.vbox.add(notebook = Gtk::Notebook.new)
2616     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
2617     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
2618                0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2619     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
2620                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2621     tooltips = Gtk::Tooltips.new
2622     tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename; for example: mplayer %f")), nil)
2623     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
2624                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2625     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)),
2626                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
2627     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)
2628     tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
2629                0, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
2630     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)
2631     tbl.attach(emptycomments_check = Gtk::CheckButton.new(utf8(_("Use empty comments for new albums"))),
2632                0, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
2633     tooltips.set_tip(emptycomments_check, utf8(_("Normally, filenames are used as comments for new albums. Check this if you prefer empty comments.")), nil)
2634     smp_check.signal_connect('toggled') {
2635         if smp_check.active?
2636             smp_hbox.sensitive = true
2637         else
2638             smp_hbox.sensitive = false
2639         end
2640     }
2641     if $config['mproc']
2642         smp_check.active = true
2643         smp_spin.value = $config['mproc'].to_i
2644     end
2645     nogestures_check.active = $config['nogestures']
2646     emptycomments_check.active = $config['emptycomments']
2647
2648     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
2649     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
2650                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
2651     tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
2652                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
2653
2654     dialog.vbox.show_all
2655     dialog.run { |response|
2656         if response == Gtk::Dialog::RESPONSE_OK
2657             $config['video-viewer'] = video_viewer_entry.text
2658             if smp_check.active?
2659                 $config['mproc'] = smp_spin.value.to_i
2660             else
2661                 $config.delete('mproc')
2662             end
2663             $config['nogestures'] = nogestures_check.active?
2664             $config['emptycomments'] = emptycomments_check.active?
2665
2666             $config['convert-enhance'] = enhance_entry.text
2667         end
2668     }
2669     dialog.destroy
2670 end
2671
2672 def perform_undo
2673     if $undo_tb.sensitive?
2674         $redo_tb.sensitive = $redo_mb.sensitive = true
2675         if not more_undoes = UndoHandler.undo($statusbar)
2676             $undo_tb.sensitive = $undo_mb.sensitive = false
2677         end
2678     end
2679 end
2680
2681 def perform_redo
2682     if $redo_tb.sensitive?
2683         $undo_tb.sensitive = $undo_mb.sensitive = true
2684         if not more_redoes = UndoHandler.redo($statusbar)
2685             $redo_tb.sensitive = $redo_mb.sensitive = false
2686         end
2687     end
2688 end
2689
2690 def show_one_click_explanation(intro)
2691     show_popup($main_window, utf8(_("<b>One-Click tools.</b>
2692
2693 %s When such a tool is activated
2694 (<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
2695 on a thumbnail will immediately apply the desired action.
2696
2697 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
2698 ") % intro))
2699 end
2700
2701 def create_menu_and_toolbar
2702
2703     #- menu
2704     mb = Gtk::MenuBar.new
2705
2706     filemenu = Gtk::MenuItem.new(utf8(_("_File")))
2707     filesubmenu = Gtk::Menu.new
2708     filesubmenu.append(new       = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
2709     filesubmenu.append(open      = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
2710     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2711     filesubmenu.append($save     = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
2712     filesubmenu.append($save_as  = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
2713     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2714     tooltips = Gtk::Tooltips.new
2715     filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
2716     $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
2717     tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
2718     filesubmenu.append($merge    = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
2719     $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
2720     tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums")), nil)
2721     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2722     filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
2723     $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
2724     tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
2725     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2726     filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
2727     tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
2728     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2729     filesubmenu.append(quit      = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
2730     filemenu.set_submenu(filesubmenu)
2731     mb.append(filemenu)
2732
2733     new.signal_connect('activate') { new_album }
2734     open.signal_connect('activate') { open_file_popup }
2735     $save.signal_connect('activate') { save_current_file_user }
2736     $save_as.signal_connect('activate') { save_as_do }
2737     $merge_current.signal_connect('activate') { merge_current }
2738     $merge.signal_connect('activate') { merge }
2739     $generate.signal_connect('activate') {
2740         save_current_file
2741         call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
2742                      utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
2743                      'web-album',
2744                      { :successmsg => utf8(_("Your web-album is now ready in directory `%s'.") % $xmldoc.root.attributes['destination']),
2745                        :failuremsg => utf8(_("There was something wrong when generating the web-album, sorry.")),
2746                        :closure_after => proc {
2747                              $xmldoc.elements.each('//dir') { |elem|
2748                                  elem.add_attribute('already-generated', 'true')
2749                              }
2750                              UndoHandler.cleanup   #- prevent save_changes to mark current dir as not already generated
2751                              $undo_tb.sensitive = $undo_mb.sensitive = false
2752                              $redo_tb.sensitive = $redo_mb.sensitive = false
2753                              save_current_file
2754                              $generated_outofline = true
2755                          }})
2756     }
2757     $properties.signal_connect('activate') { properties }
2758
2759     quit.signal_connect('activate') { try_quit }
2760
2761     editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
2762     editsubmenu = Gtk::Menu.new
2763     editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
2764     editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
2765     editsubmenu.append(           Gtk::SeparatorMenuItem.new)
2766     editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
2767     $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
2768     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)
2769     editsubmenu.append(           Gtk::SeparatorMenuItem.new)
2770     editsubmenu.append(prefs    = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
2771     editmenu.set_submenu(editsubmenu)
2772     mb.append(editmenu)
2773
2774     $remove_all_captions.signal_connect('activate') { remove_all_captions }
2775
2776     prefs.signal_connect('activate') { preferences }
2777
2778     helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
2779     helpsubmenu = Gtk::Menu.new
2780     helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
2781     one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
2782     helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
2783     speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
2784     helpsubmenu.append(        Gtk::SeparatorMenuItem.new)
2785     helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
2786     helpmenu.set_submenu(helpsubmenu)
2787     mb.append(helpmenu)
2788
2789     one_click.signal_connect('activate') {
2790         show_one_click_explanation(_("One-Click tools are available in the toolbar."))
2791     }
2792
2793     speed.signal_connect('activate') {
2794         show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
2795
2796 <span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
2797 <span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
2798 <span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
2799 <span foreground='darkblue'>Control-Enter</span>: for an image, open larger view; for a video, launch player
2800 <span foreground='darkblue'>Control-Delete</span>: delete image
2801 <span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
2802 <span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
2803 <span foreground='darkblue'>Control-z</span>: undo
2804 <span foreground='darkblue'>Control-r</span>: redo
2805
2806 <span size='large' weight='bold'>Mouse gestures:</span>
2807
2808 Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
2809 for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
2810
2811 <span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
2812 <span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
2813 <span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
2814 <span foreground='darkblue'>Left click, hold left button, right click</span>: undo
2815 <span foreground='darkblue'>Right click, hold right button, left click</span>: redo
2816 ")), { :pos_centered => true, :not_transient => true })
2817     }
2818
2819
2820     about.signal_connect('activate') {
2821         show_popup($main_window, utf8(_("<span size='x-large' weight='bold'>Booh %s</span>
2822
2823 <i>``The Web-Album of choice for discriminating Linux users''</i>
2824
2825 Copyright (c) 2005 Guillaume Cottenceau
2826
2827 Translations:
2828 Japanese: Masao Mutoh
2829 French: Guillaume Cottenceau") % $VERSION), { :centered => true, :pos_centered => true })
2830     }
2831
2832
2833     #- toolbar
2834     tb = Gtk::Toolbar.new
2835
2836     tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
2837     open.label = utf8(_("Open"))  #- to avoid missing gtk2 l10n catalogs
2838     open.menu = Gtk::Menu.new
2839     open.signal_connect('clicked') { open_file_popup }
2840     open.signal_connect('show-menu') {
2841         lastopens = Gtk::Menu.new
2842         j = 0
2843         if $config['last-opens']
2844             $config['last-opens'].reverse.each { |e|
2845                 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
2846                 item.signal_connect('activate') {
2847                     if ask_save_modifications(utf8(_("Save this album?")),
2848                                               utf8(_("Do you want to save the changes to this album?")),
2849                                               { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2850                         push_mousecursor_wait
2851                         msg = open_file_user(from_utf8(e))
2852                         pop_mousecursor
2853                         if msg
2854                             show_popup($main_window, msg)
2855                         end
2856                     end
2857                 }
2858                 j += 1
2859             }
2860             lastopens.show_all
2861         end
2862         open.menu = lastopens
2863     }
2864
2865     tb.insert(-1, Gtk::SeparatorToolItem.new)
2866
2867     tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
2868     $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
2869     $r90.label = utf8(_("Rotate"))
2870     tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
2871     $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
2872     $r270.label = utf8(_("Rotate"))
2873     tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
2874     $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
2875     $enhance.label = utf8(_("Enhance"))
2876     tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
2877     $delete.label = utf8(_("Delete"))  #- to avoid missing gtk2 l10n catalogs
2878     tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
2879     nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
2880     nothing.label = utf8(_("None"))
2881
2882     tb.insert(-1, Gtk::SeparatorToolItem.new)
2883
2884     tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
2885     tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
2886
2887
2888     $undo_tb.signal_connect('clicked')  { perform_undo }
2889     $undo_mb.signal_connect('activate') { perform_undo }
2890     $redo_tb.signal_connect('clicked')  { perform_redo }
2891     $redo_mb.signal_connect('activate') { perform_redo }
2892
2893     one_click_explain_try = Proc.new {
2894         if !$config['one-click-explained']
2895             show_one_click_explanation(_("You have just clicked on a One-Click tool."))
2896             $config['one-click-explained'] = true
2897         end
2898     }
2899
2900     $r90.signal_connect('toggled') {
2901         if $r90.active?
2902             set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
2903             one_click_explain_try.call
2904             $r270.active = false
2905             $enhance.active = false
2906             $delete.active = false
2907             nothing.sensitive = true
2908         else
2909             if !$r270.active? && !$enhance.active? && !$delete.active?
2910                 set_mousecursor_normal
2911                 nothing.sensitive = false
2912             else
2913                 nothing.sensitive = true
2914             end
2915         end
2916     }
2917     $r270.signal_connect('toggled') {
2918         if $r270.active?
2919             set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
2920             one_click_explain_try.call
2921             $r90.active = false
2922             $enhance.active = false
2923             $delete.active = false
2924             nothing.sensitive = true
2925         else
2926             if !$r90.active? && !$enhance.active? && !$delete.active?
2927                 set_mousecursor_normal
2928                 nothing.sensitive = false
2929             else
2930                 nothing.sensitive = true
2931             end
2932         end
2933     }
2934     $enhance.signal_connect('toggled') {
2935         if $enhance.active?
2936             set_mousecursor(Gdk::Cursor::SPRAYCAN)
2937             one_click_explain_try.call
2938             $r90.active = false
2939             $r270.active = false
2940             $delete.active = false
2941             nothing.sensitive = true
2942         else
2943             if !$r90.active? && !$r270.active? && !$delete.active?
2944                 set_mousecursor_normal
2945                 nothing.sensitive = false
2946             else
2947                 nothing.sensitive = true
2948             end
2949         end
2950     }
2951     $delete.signal_connect('toggled') {
2952         if $delete.active?
2953             set_mousecursor(Gdk::Cursor::PIRATE)
2954             one_click_explain_try.call
2955             $r90.active = false
2956             $r270.active = false
2957             $enhance.active = false
2958             nothing.sensitive = true
2959         else
2960             if !$r90.active? && !$r270.active? && !$enhance.active?
2961                 set_mousecursor_normal
2962                 nothing.sensitive = false
2963             else
2964                 nothing.sensitive = true
2965             end
2966         end
2967     }
2968     nothing.signal_connect('clicked') {
2969         $r90.active = $r270.active = $enhance.active = $delete.active = false
2970         set_mousecursor_normal
2971     }
2972
2973     return [ mb, tb ]
2974 end
2975
2976 def gtk_thread_protect(&proc)
2977     if Thread.current == Thread.main
2978         proc.call
2979     else
2980         $protect_gtk_pending_calls.synchronize {
2981             $gtk_pending_calls << proc
2982         }
2983     end
2984 end
2985
2986 def gtk_thread_abandon
2987     $protect_gtk_pending_calls.try_lock
2988     $gtk_pending_calls = []
2989     $protect_gtk_pending_calls.unlock
2990 end
2991
2992 def create_main_window
2993
2994     mb, tb = create_menu_and_toolbar
2995
2996     $albums_tv = Gtk::TreeView.new
2997     $albums_tv.set_size_request(120, -1)
2998     renderer = Gtk::CellRendererText.new
2999     column = Gtk::TreeViewColumn.new('', renderer, { :text => 0 })
3000     $albums_tv.append_column(column)
3001     $albums_tv.set_headers_visible(false)
3002     $albums_tv.selection.signal_connect('changed') { |w|
3003         push_mousecursor_wait
3004         save_changes
3005         iter = w.selected
3006         if !iter
3007             msg 3, "no selection"
3008         else
3009             $current_path = $albums_ts.get_value(iter, 1)
3010             change_dir
3011         end
3012         pop_mousecursor
3013     }
3014     $albums_ts = Gtk::TreeStore.new(String, String)
3015     $albums_tv.set_model($albums_ts)
3016     $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
3017
3018     albums_sw = Gtk::ScrolledWindow.new(nil, nil)
3019     albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
3020     albums_sw.add_with_viewport($albums_tv)
3021
3022     $notebook = Gtk::Notebook.new
3023     create_subalbums_page
3024     $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
3025     create_auto_table
3026     $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
3027     $notebook.show_all
3028     $notebook.signal_connect('switch-page') { |w, page, num|
3029         if num == 0
3030             $delete.active = false
3031             $delete.sensitive = false
3032         else
3033             $delete.sensitive = true
3034         end
3035         if $xmldir && $subalbums_edits[$xmldir.attributes['path']] && textview = $subalbums_edits[$xmldir.attributes['path']][:editzone]
3036             if num == 0
3037                 textview.buffer.text = $thumbnails_title.buffer.text
3038             else
3039                 if $notebook.get_tab_label($autotable_sw).sensitive?
3040                     $thumbnails_title.buffer.text = textview.buffer.text
3041                 end
3042             end
3043         end
3044     }
3045
3046     paned = Gtk::HPaned.new
3047     paned.pack1(albums_sw, false, false)
3048     paned.pack2($notebook, true, true)
3049
3050     main_vbox = Gtk::VBox.new(false, 0)
3051     main_vbox.pack_start(mb, false, false)
3052     main_vbox.pack_start(tb, false, false)
3053     main_vbox.pack_start(paned, true, true)
3054     main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
3055
3056     $main_window = Gtk::Window.new
3057     $main_window.add(main_vbox)
3058     $main_window.signal_connect('delete-event') {
3059         try_quit({ :disallow_cancel => true })
3060     }
3061
3062     #- read/save size and position of window
3063     if $config['pos-x'] && $config['pos-y']
3064         $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
3065     else
3066         $main_window.window_position = Gtk::Window::POS_CENTER
3067     end
3068     msg 3, "size: #{$config['width']}x#{$config['height']}"
3069     $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
3070     $main_window.signal_connect('configure-event') {
3071         msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
3072         x, y = $main_window.window.root_origin
3073         width, height = $main_window.window.size
3074         $config['pos-x'] = x
3075         $config['pos-y'] = y
3076         $config['width'] = width
3077         $config['height'] = height
3078         false
3079     }
3080
3081     $protect_gtk_pending_calls = Mutex.new
3082     $gtk_pending_calls = []
3083     Gtk.timeout_add(100) {
3084         $protect_gtk_pending_calls.synchronize {
3085             $gtk_pending_calls.each { |c| c.call }
3086             $gtk_pending_calls = []
3087         }
3088         true
3089     }
3090
3091     $statusbar.push(0, utf8(_("Ready.")))
3092     $main_window.show_all
3093 end
3094
3095 Thread.abort_on_exception = true
3096
3097 handle_options
3098 read_config
3099
3100 Gtk.init
3101 create_main_window
3102 if ARGV[0]
3103     open_file_user(ARGV[0])
3104 end
3105 Gtk.main
3106
3107 write_config