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