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