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