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