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