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