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