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